Mô đun:Map
Giao diện

Bạn có thể muốn tạo một trang tài liệu cho mô đun Scribunto này. Biên tập viên sửa đổi có thể thử nghiệm trong các trang chỗ thử (tạo | sao) và trường hợp kiểm thử (tạo) của mô đun này. Các trang con của mô đun này. |
local getArgs = require('Module:Arguments').getArgs
local p = {}
function dbg(v, msg)
mw.log((msg or '') .. mw.text.jsonEncode(v))
end
-- Parse all unnamed string parameters in a form of "latitude, longitude" into the real number pairs
function getSequence(args)
local coords = {}
for ind, val in pairs( args ) do
if type(ind) == "number" then
local valid = false
local val2 = mw.text.split( val, ',', true )
-- allow for elevation
if #val2 >= 2 and #val2 <= 3 then
local lat = tonumber(val2[1])
local lon = tonumber(val2[2])
if lat ~= nil and lon ~= nil then
table.insert(coords, { lon, lat } )
valid = true
end
end
if not valid then error('Unnamed parameter #' .. ind .. ' "' .. val .. '" is not recognized as a valid "latitude,longitude" value') end
end
end
return coords
end
-- See http://geojson.org/geojson-spec.html
-- Convert a comma and semicolon separated numbers into geojson coordinate arrays
-- Each geotype expects a certain array depth:
-- Point - [ lon, lat ] All other types use point as the basic type
-- MultiPoint - array of points: [ point, ... ]
-- LineString - array of 2 or more points: [ point, point, ... ]
-- MultiLineString - array of LineStrings: [ [ point, point, ... ], ... ]
-- Polygon - [ [ point, point, point, point, ... ], ... ]
-- each LinearRing is an array of 4 or more points, where first and last must be the same
-- first LinearRing is the exterior ring, subsequent rings are holes in it
-- MultiPolygon - array of Polygons: [ [ [ point, point, point, point, ... ], ... ], ... ]
--
-- For example, for the LineString, data "p1;p2;p3" would be converted to [p1,p2,p3] (each "p" is a [lon,lat] value)
-- LineString has the depth of "1" -- array of points (each point being a two value array)
-- For Polygon, the same sequence "p1;p2;p3" would be converted to [[p1,p2,p3]]
-- Which is an array of array of points. But sometimes we need to specify two subarrays of points:
-- [[p1,p2],[p3]] (last point is in a separate array), and we do it with "p1;p2;;p3"
-- Similarly, for MultiPolygon, "p1;p2;;;p3" would generate [[[p1,p2]],[[p3]]]
--
function p.parseGeoSequence(args)
local result = p._parseGeoSequence(args)
if type(result) == 'string' then error(result) end
return result
end
function p._parseGeoSequence(args)
local allTypes = {
-- how many nested array levels until we get to the Point,
-- second is the minimum number of values each Points array must have
Point = { 1, 1 },
MultiPoint = { 1, 0 },
LineString = { 1, 2 },
MultiLineString = { 2, 2 },
Polygon = { 2, 4 },
MultiPolygon = { 3, 4 },
}
if not allTypes[args.geotype] then return ('Unknown geotype ' .. args.geotype) end
local levels, min = unpack(allTypes[args.geotype])
local result
result = {}
for i = 1, levels do result[i] = {} end
local gap = 0
-- Example for levels==3, converting "p1 ; p2 ; ; ; p3 ; ; p4" => [[[p1, p2]], [[p3],[p4]]]
-- This function will be called after each gap, and all values are done, so the above will call:
-- before p3: gap=2, [],[],[p1,p2] => [[[p1,p2]]],[],[]
-- before p4: gap=1, [[[p1,p2]]],[],[p3] => [[[p1,p2]]],[[p3]]],[]
-- the end, gap=2, [[[p1,p2]]],[[p3]]],[p4] => [[[p1,p2]],[[p3],[p4]]],[],[]
-- Here, convert at "p1 ; ; " from [[],[p1]]
local closeArrays = function (gap)
if #result[levels] < min then
error('Each points array must be at least ' .. min .. ' values')
elseif min == 1 and #result[levels] ~= 1 then
-- Point
error('Point must have exactly one data point')
end
-- attach arrays in reverse order to the higher order ones
for i = levels, levels-gap+1, -1 do
table.insert(result[i-1], result[i])
result[i] = {}
end
return 0
end
local usedSequence = false
for val in mw.text.gsplit(args.data, ';', true) do
local val2 = mw.text.split(val, ',', true)
-- allow for elevation
if #val2 >= 2 and #val2 <= 3 and not usedSequence then
if gap > 0 then gap = closeArrays(gap) end
local lat = tonumber(val2[1])
local lon = tonumber(val2[2])
if lat == nil or lon == nil then return ('Bad data value "' .. val .. '"') end
table.insert(result[levels], { lon, lat } )
else
val = mw.text.trim(val)
if val == '' then
usedSequence = false
gap = gap + 1
if (gap >= levels) then return ('Data must not skip more than ' .. levels-1 .. ' values') end
elseif usedSequence then
return ('Coordinates may not be added right after the named sequence')
else
if gap > 0 then
gap = closeArrays(gap)
elseif #result[levels] > 0 then
return ('Named sequence "' .. val .. '" cannot be used in the middle of the sequence')
end
-- Parse value as a sequence name. Eventually we can load data from external data sources
if val == 'values' then
val = getSequence(args)
elseif min == 4 and val == 'world' then
val = {{36000,-180}, {36000,180}, {-36000,180}, {-36000,-180}, {36000,-180}}
elseif tonumber(val) ~= nil then
return ('Not a valid coordinate or a sequence name: ' .. val)
else
return ('Sequence "' .. val .. '" is not known. Try "values" or "world" (for Polygons), or specify values as lat,lon;lat,lon;... pairs')
end
result[levels] = val
usedSequence = true
end
end
end
-- allow one empty last value (some might close the list with an extra semicolon)
if (gap > 1) then return ('Data values must not have blanks at the end') end
closeArrays(levels-1)
return args.geotype == 'Point' and result[1][1] or result[1]
end
-- Run this function to check that the above works ok
function p.parseGeoSequenceTest()
local testSeq = function(data, expected)
local result = getSequence(data)
if type(result) == 'table' then
local actual = mw.text.jsonEncode(result)
result = actual ~= expected and 'data="' .. mw.text.jsonEncode(data) .. '", actual="' .. actual .. '", expected="' .. expected .. '"<br>\n' or ''
else
result = result .. '<br>\n'
end
return result
end
local test = function(geotype, data, expected, values)
values = values or {}
values.geotype = geotype;
values.data = data;
local result = p._parseGeoSequence(values)
if type(result) == 'table' then
local actual = mw.text.jsonEncode(result)
result = actual ~= expected and 'geotype="' .. geotype .. '", data="' .. data .. '", actual="' .. actual .. '", expected="' .. expected .. '"<br>\n' or ''
else
result = 'geotype="' .. geotype .. '", data="' .. data .. '", error="' .. result .. '<br>\n'
end
return result
end
local values = {' 9 , 8 ','7,6'}
local result = '' ..
testSeq({}, '[]') ..
testSeq({'\t\n 1 \r,-10'}, '[[-10,1]]') ..
testSeq(values, '[[8,9],[6,7]]') ..
test('Point', '1,2', '[2,1]') ..
test('MultiPoint', '1,2;3,4;5,6', '[[2,1],[4,3],[6,5]]') ..
test('LineString', '1,2;3,4', '[[2,1],[4,3]]') ..
test('MultiLineString', '1,2;3,4', '[[[2,1],[4,3]]]') ..
test('MultiLineString', '1,2;3,4;;5,6;7,8', '[[[2,1],[4,3]],[[6,5],[8,7]]]') ..
test('Polygon', '1,2;3,4;5,6;1,2', '[[[2,1],[4,3],[6,5],[2,1]]]') ..
test('MultiPolygon', '1,2;3,4;5,6;1,2', '[[[[2,1],[4,3],[6,5],[2,1]]]]') ..
test('MultiPolygon', '1,2;3,4;5,6;1,2;;11,12;13,14;15,16;11,12', '[[[[2,1],[4,3],[6,5],[2,1]],[[12,11],[14,13],[16,15],[12,11]]]]') ..
test('MultiPolygon', '1,2;3,4;5,6;1,2;;;11,12;13,14;15,16;11,12', '[[[[2,1],[4,3],[6,5],[2,1]]],[[[12,11],[14,13],[16,15],[12,11]]]]') ..
test('MultiPolygon', '1,2;3,4;5,6;1,2;;;11,12;13,14;15,16;11,12;;21,22;23,24;25,26;21,22', '[[[[2,1],[4,3],[6,5],[2,1]]],[[[12,11],[14,13],[16,15],[12,11]],[[22,21],[24,23],[26,25],[22,21]]]]') ..
test('MultiLineString', 'values;;1,2;3,4', '[[[8,9],[6,7]],[[2,1],[4,3]]]', values) ..
test('Polygon', 'world;;world', '[[[36000,-180],[36000,180],[-36000,180],[-36000,-180],[36000,-180]],[[36000,-180],[36000,180],[-36000,180],[-36000,-180],[36000,-180]]]') ..
''
return result ~= '' and result or 'Tests passed'
end
function p._tag(args)
local tagname = args.type or 'maplink'
if tagname ~= 'maplink' and tagname ~= 'mapframe' then error('unknown type "' .. tagname .. '"') end
local geojson
local tagArgs = {
text = args.text,
zoom = tonumber(args.zoom),
latitude = tonumber(args.latitude),
longitude = tonumber(args.longitude),
group = args.group,
show = args.show,
class = args.class,
}
if tagname == 'mapframe' then
tagArgs.width = args.width == nil and 420 or args.width
tagArgs.height = args.height == nil and 420 or args.height
tagArgs.align = args.align == nil and 'right' or args.align
elseif not args.class and (args.text == '' or args.text == '""') then
-- Hide pushpin icon in front of an empty text link
tagArgs.class = 'no-icon'
end
if args.data == '' then args.data = nil end
if (not args.geotype) ~= (not args.data) then
-- one is given, but not the other
if args.data then
error('Parameter "data" is given, but "geotype" is not set. Use one of these: Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon')
elseif args.geotype == "Point" and tagArgs.latitude ~= nil and tagArgs.longitude ~= nil then
-- For Point geotype, it is enough to set latitude and logitude, and data will be set up automatically
args.data = tagArgs.latitude .. ',' .. tagArgs.longitude
else
error('Parameter data must be set. Use "values" to use all unnamed parameters as coordinates (lat,lon|lat,lon|...), "world" for the whole world, a combination to make a mask, e.g. "world;;values", or direct values "lat,lon;lat,lon..." with ";" as value separator')
end
end
-- Kartographer can now automatically calculate needed zoom & lat/long based on the data provided
-- Current version ignores mapmasks, but that will also be fixed soon. Leaving this for now, but can be removed if all is good.
-- tagArgs.zoom = tagArgs.zoom == nil and 14 or tagArgs.zoom
-- tagArgs.latitude = tagArgs.latitude == nil and 51.47766 or tagArgs.latitude
-- tagArgs.longitude = tagArgs.longitude == nil and -0.00115 or tagArgs.longitude
if args.image then
args.description = (args.description or '') .. '[[file:' .. args.image .. '|300px]]'
end
if args.geotype then
geojson = {
type = "Feature",
properties = {
title = args.title,
description = args.description,
['marker-size'] = args['marker-size'],
['marker-symbol'] = args['marker-symbol'],
['marker-color'] = args['marker-color'],
stroke = args.stroke,
['stroke-opacity'] = tonumber(args['stroke-opacity']),
['stroke-width'] = tonumber(args['stroke-width']),
fill = args.fill,
['fill-opacity'] = tonumber(args['fill-opacity']),
},
geometry = {
type = args.geotype,
coordinates = p.parseGeoSequence(args)
}
}
end
if args.debug ~= nil then
local html = mw.html.create(tagname, not geojson and {selfClosing=true} or nil)
:attr(tagArgs)
if geojson then
html:wikitext( mw.text.jsonEncode(geojson, mw.text.JSON_PRETTY) )
end
return 'syntaxhighlight', tostring(html) .. mw.text.jsonEncode(args, mw.text.JSON_PRETTY), { lang = 'json' }
end
return tagname, geojson and mw.text.jsonEncode(geojson) or '', tagArgs
end
function p.tag(frame)
local args = getArgs(frame)
local tag, geojson, tagArgs = p._tag(args)
return frame:extensionTag(tag, geojson, tagArgs)
end
return p