Module:Map: Difference between revisions

From Wikivoyage
Jump to navigation Jump to search
Content deleted Content added
No edit summary
No edit summary
Line 161: Line 161:
if type == 'maplink' or type == 'mapframe' then
if type == 'maplink' or type == 'mapframe' then
tagArgs.zoom = tagArgs.zoom == nil and 14 or tagArgs.zoom
tagArgs.zoom = tagArgs.zoom == nil and 14 or tagArgs.zoom
tagArgs.latitude = tagArgs.latitude == nil and 51.47766 or tagArgs.latitude
tagArgs.latitude = tagArgs.latitude == nil and 51.47766 or tagArgs.latitude
tagArgs.longitude = tagArgs.longitude == nil and -0.00115 or tagArgs.longitude
tagArgs.longitude = tagArgs.longitude == nil and -0.00115 or tagArgs.longitude
if type == 'mapframe' then
if type == 'mapframe' then
Line 204: Line 204:
type = 'syntaxhighlight'
type = 'syntaxhighlight'
geojson = {
geojson = {
args = tagArgs,
params = args,
tagArgs = tagArgs,
geojson = geojson,
geojson = geojson,
}
}

Revision as of 03:59, 10 June 2016

[view] [edit] [history] [purge] Documentation

This module provides input parameters for mapframe and maplink functions supported by Extension:Kartographer.

Usage:

{{#invoke:map|tag|type=maplink|geotype=Point|title=Example|latitude=59.0|longitude=29.0}}
Parameters for Module:Map
Parameter Usage
type maplink oder mapframe depending on which function should be invoked
geotype Point for individual points, Polygon for polygons
title Object name
latitude and longitude use 'NA' to disable coordinates, including the ones from wikidata
zoom Zoom level of the map
marker-symbol Symbol, letter, or number for displaying on the map as marker
marker-color Color of the map marker
group Group of markers (see, eat, drink, etc.)
show Which marker groups to show (by default shows the most common groups like see, eat, drink, ...)
data data=values fills the polygon given by data
data=world;;values fills the area outside of the polygon
image Name of the image shown in the thumbnail
width and height map width and map height in px or % of screen width, only for mapframe
wikidata if specified, the missing title/lat/long/image fields will be fetched from the respective wikidata entry fields

local getArgs = require('Module:Arguments').getArgs
local p = {}

function extractCoordinates(args)
  local coords = {}
  local namedCoords = { unnamed = coords }

  -- Parse an array of strings of number pairs "latitude, longitude" into the real number pairs
  for k, v in pairs( args ) do
    if type(k) == "number" then
      local v2 = mw.text.split( v, ',', true )
      if #v2 == 2 then
        local lat = tonumber(v2[1])
        local lon = tonumber(v2[2])
        if lat ~= nil and lon ~= nil then
            table.insert(coords, { lon, lat } )
        end
      end
    end
  end
  return namedCoords
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, value "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.parseCoordinateParam(geotype, value, namedCoords)
  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[geotype] then error('Unknown geotype ' .. geotype) end
  local levels, min = unpack(allTypes[geotype])

  if min == 4 then
    -- Cover the whole world
    namedCoords.world = {{36000,-180}, {36000,180}, {-36000,180}, {-36000,-180}, {36000,-180}}
  end

  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')
    elseif min == 4 then
      -- Polygon or MultiPolygon
      local lon1, lat1 = unpack(result[levels][1])
      local lon2, lat2 = unpack(result[levels][#result[levels]])
      if (lon1 ~= lon2 or lat1 ~= lat2) then
        error("Polygon's first and last points must be the same")
      end
    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
  end

  if value ~= '' then
    local namedRangeUsed = false
    for v in mw.text.gsplit(value, ';', true) do
      local v2 = mw.text.split( v, ',', true )
      if #v2 == 2 and not namedRangeUsed then
        if gap > 0 then
          closeArrays(gap)
          gap = 0
        end
        local lat = tonumber(v2[1])
        local lon = tonumber(v2[2])
        if lat == nil or lon == nil then error('Bad data value "' .. v .. '"') end
        table.insert(result[levels], { lon, lat } )
      else
        v = mw.text.trim(v)
        if v == '' then
          namedRangeUsed = false
          gap = gap + 1
          if (gap >= levels) then error('Data must not skip more than ' .. levels-1 .. ' values') end
        elseif namedRangeUsed then
          error('Coordinates may not be added after named range was used')
        elseif tonumber(v) ~= nil then
          error('Not a valid coordinate: ' .. v)
        elseif #result[levels] > 0 then
          error('Named range "' .. v .. '" cannot be used in the middle of the sequence')
        elseif not namedCoords[v] then
          error('Named range "' .. v .. '" is not known. Try "unnamed", "world"')
        else
          result[levels] = namedCoords[v]
        end
      end
    end
    if (gap > 0) then error('Data values must not have blanks at the end') end
  end
  closeArrays(levels-1)
  return geotype == 'Point' and result[1][1] or result[1]
end

-- Run this function to check that the above works ok
function p._debug_parseCoordinateParam()
  assert(mw.text.jsonEncode(p.parseCoordinateParam('MultiPoint', '')) == '[]')
  assert(mw.text.jsonEncode(p.parseCoordinateParam('Point', '1,2')) == '[2,1]')
  assert(mw.text.jsonEncode(p.parseCoordinateParam('MultiPoint', '1,2;3,4;5,6')) == '[[2,1],[4,3],[6,5]]')
  assert(mw.text.jsonEncode(p.parseCoordinateParam('LineString', '1,2;3,4')) == '[[2,1],[4,3]]')
  assert(mw.text.jsonEncode(p.parseCoordinateParam('MultiLineString', '1,2;3,4')) == '[[[2,1],[4,3]]]')
  assert(mw.text.jsonEncode(p.parseCoordinateParam('MultiLineString', '1,2;3,4;;5,6;7,8')) == '[[[2,1],[4,3]],[[6,5],[8,7]]]')
  assert(mw.text.jsonEncode(p.parseCoordinateParam('MultiPolygon', '1,2;3,4;5,6;1,2')) == '[[[[2,1],[4,3],[6,5],[2,1]]]]')
  assert(mw.text.jsonEncode(p.parseCoordinateParam('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]]]]')
  assert(mw.text.jsonEncode(p.parseCoordinateParam('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]]]]')
  assert(mw.text.jsonEncode(p.parseCoordinateParam('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]]]]')
  assert(mw.text.jsonEncode(p.parseCoordinateParam('MultiLineString', 'aa;;1,2;3,4',{aa={{9,8},{7,6}}}) == '[[[9,8],[7,6]],[[2,1],[4,3]]]'))
end


function p._tag(args)
  local type = args.type or 'maplink'
  local namedCoords = extractCoordinates(args)
  local tagArgs = {
    text = args.text,
    zoom = tonumber(args.zoom),
    latitude = tonumber(args.latitude),
    longitude = tonumber(args.longitude),
    group = args.group,
    show = args.show,
  }
  local geojson

  if type == 'maplink' or type == 'mapframe' then
    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 type == 'mapframe' then
      tagArgs.width = tagArgs.width == nil and 420 or tagArgs.width
      tagArgs.height = tagArgs.height == nil and 420 or tagArgs.height
    end
  else
    error('unknown type')
  end

  if args.geotype then
    if args.geotype == "Point" and #namedCoords.unnamed == 0 and not args.data then
      if tagArgs.latitude ~= nil and tagArgs.longitude ~= nil then
        args.data = { tagArgs.latitude, tagArgs.longitude }
      end
    end

    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.parseCoordinateParam(args.geotype, args.data, namedCoords )
      }
    }
  end

  -- Debug support -- simply supply a debug field for a pretty print
  local flags
  if args.debug ~= nil then
    type = 'syntaxhighlight'
    geojson = {
      params = args,
      tagArgs = tagArgs,
      geojson = geojson,
    }
    tagArgs = { lang = 'json' }
    flags = mw.text.JSON_PRETTY
  end

  return type, geojson and mw.text.jsonEncode(geojson, flags) or '', tagArgs
end

function p.tag(frame)
  local args = getArgs(frame, { parentFirst = true })
  local tag, geojson, tagArgs = p._tag(args)
  return frame:extensionTag( tag, geojson, tagArgs)
end

return p