Модуль:Map

Материал из ultracity
Перейти к навигации Перейти к поиску

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 .. '"
\n' or else result = result .. '
\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 .. '"
\n' or else result = 'geotype="' .. geotype .. '", data="' .. data .. '", error="' .. result .. '
\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,
       lang = args.lang,
   }
   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 ) .. 'Файл:' .. args.image .. '' 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