Module:Mapframe: Difference between revisions
Content deleted Content added
Update from sandbox. Pre-expand numeric arguments. Significant runtime improvement on large maps. |
Copy from sandbox. Simplify code. Also, allow all supplied values to be considered even if non-sequential feature numbering has been used. |
||
Line 164: | Line 164: | ||
end |
end |
||
local content = {} |
local content = {} |
||
local contentIndex = ''; |
|||
local argsExpanded = {} |
local argsExpanded = {} |
||
for k, v in pairs(args) do |
for k, v in pairs(args) do |
||
local index = string.match( k, '^[^0-9]+([0-9]*)$' ) |
local index = string.match( k, '^[^0-9]+([0-9]*)$' ) |
||
if index ~= nil then |
if index ~= nil then |
||
local indexNumber = '' |
local indexNumber = '' |
||
if index ~= '' then |
if index ~= '' then |
||
indexNumber = tonumber(index) |
indexNumber = tonumber(index) |
||
else |
|||
indexNumber = 1 |
|||
end |
end |
||
Line 183: | Line 184: | ||
end |
end |
||
for contentIndex, contentArgs in pairs(argsExpanded) do |
|||
local nextTypeOrFromExists = getParameterValue(args, 'type') or getParameterValue(args, 'from') |
|||
while nextTypeOrFromExists do |
|||
local contentArgs = argsExpanded[contentIndex] |
|||
-- Kartographer automatically calculates coords if geolines/shapes are used (T227402) |
-- Kartographer automatically calculates coords if geolines/shapes are used (T227402) |
||
if not coordsDerivedFromFeatures then |
if not coordsDerivedFromFeatures then |
||
Line 193: | Line 191: | ||
end |
end |
||
if contentIndex == '' then contentIndex = 1 end |
|||
content[contentIndex] = makeContentJson(contentArgs) |
content[contentIndex] = makeContentJson(contentArgs) |
||
contentIndex = contentIndex + 1 |
|||
nextTypeOrFromExists = getParameterValue(args, 'type', contentIndex) or getParameterValue(args, 'from', contentIndex) |
|||
end |
end |
||
Revision as of 23:59, 7 June 2020
This module is rated as beta, and is ready for widespread use. It is still new and should be used with some caution to ensure the results are as expected. |
This Lua module is used on approximately 426,000 pages, or roughly 1% of all pages. To avoid major disruption and server load, any changes should be tested in the module's /sandbox or /testcases subpages, or in your own module sandbox. The tested changes can be added to this page in a single edit. Consider discussing changes on the talk page before implementing them. |
This module depends on the following other modules: |
This module uses TemplateStyles: |
On English Wikipedia, this module is called by {{Maplink}}
, see that template's documentation for usage instructions.
Usage
- Standard usage
- Just use {{Maplink}}, which passes its parameters to this module's main function.
- From another module
-
- Import this module, e.g.
local mf = require('Module:Mapframe')
- Pass a table of parameter names/values to the _main function. See {{Maplink}} documentation for parameter names and descriptions. E.g.
local mapframe = mf._main(parameters)
- Preprocess _main's output before returning it, e.g.
return frame:preprocess(mapframe)
- Import this module, e.g.
Set up on another wiki
- Create template and module:
- Import this module and its template to that wiki (or copy the code over, giving attribution in the edit summary). Optionally, give them a name that makes sense in that wiki's language
- On Wikidata, add them to the items Module:Mapframe (Q52554979) and Template:Maplink (Q27882107)
- Localise the module
- Edit the top bits of the module, between the comments
-- ##### Localisation (L10n) settings #####
and-- #### End of L10n settings ####
, replacing values between"
"
symbols with local values (when necessary)
- Edit the top bits of the module, between the comments
- Add documentation
- to the template (e.g. by translating Template:Maplink/doc, adjusting as necessary per any localisations made in the previous step)
- to the module (please transfer/translate these instructions so that wikimedians who read your wiki but not the English Wikipedia can also set up the module and template on another wiki).
-- Note: Originally written on English Wikipedia at https://en.wikipedia.org/wiki/Module:Mapframe
-- ##### Localisation (L10n) settings #####
-- Replace values in quotes ("") with localised values
local L10n = {}
-- Template parameter names (unnumbered versions only)
-- Specify each as either a single string, or a table of strings (aliases)
-- Aliases are checked left-to-right, i.e. `{ "one", "two" }` is equivalent to using `{{{one| {{{two|}}} }}}` in a template
L10n.para = {
display = "display",
type = "type",
id = { "id", "ids" },
from = "from",
raw = "raw",
title = "title",
description = "description",
strokeColor = { "stroke-color", "stroke-colour" },
strokeWidth = "stroke-width",
strokeOpacity = "stroke-opacity",
fill = "fill",
fillOpacity = "fill-opacity",
coord = "coord",
marker = "marker",
markerColor = { "marker-color", "marker-colour" },
markerSize = "marker-size",
radius = { "radius", "radius_m" },
radiusKm = "radius_km",
radiusFt = "radius_ft",
radiusMi = "radius_mi",
edges = "edges",
text = "text",
icon = "icon",
zoom = "zoom",
frame = "frame",
plain = "plain",
frameWidth = "frame-width",
frameHeight = "frame-height",
frameCoordinates = { "frame-coordinates", "frame-coord" },
frameLatitude = { "frame-lat", "frame-latitude" },
frameLongitude = { "frame-long", "frame-longitude" },
frameAlign = "frame-align"
}
-- Names of other templates this module depends on
L10n.template = {
Coord = "Coord"
}
-- Error messages
L10n.error = {
badDisplayPara = "Invalid display parameter",
noCoords = "Coordinates must be specified on Wikidata or in |" .. ( type(L10n.para.coord)== 'table' and L10n.para.coord[1] or L10n.para.coord ) .. "=",
wikidataCoords = "Coordinates not found on Wikidata"
}
-- Other strings
L10n.str = {
-- valid values for display parameter, e.g. (|display=inline) or (|display=title) or (|display=inline,title) or (|display=title,inline)
inline = "inline",
title = "title",
dsep = ",", -- separator between inline and title (comma in the example above)
-- valid values for type paramter
line = "line", -- geoline feature (e.g. a road)
shape = "shape", -- geoshape feature (e.g. a state or province)
shapeInverse = "shape-inverse", -- geomask feature (the inverse of a geoshape)
data = "data", -- geoJSON data page on Commons
point = "point", -- single point feature (coordinates)
circle = "circle", -- circular area around a point
-- valid values for icon, frame, and plain parameters
affirmedWords = ' '..table.concat({
"add",
"added",
"affirm",
"affirmed",
"include",
"included",
"on",
"true",
"yes",
"y"
}, ' ')..' ',
declinedWords = ' '..table.concat({
"decline",
"declined",
"exclude",
"excluded",
"false",
"none",
"not",
"no",
"n",
"off",
"omit",
"omitted",
"remove",
"removed"
}, ' ')..' '
}
-- Default values for parameters
L10n.defaults = {
display = L10n.str.inline,
text = "Map",
frameWidth = "300",
frameHeight = "200",
markerColor = "5E74F3",
markerSize = nil,
strokeColor = "#ff0000",
strokeWidth = 6,
edges = 32 -- number of edges used to approximate a circle
}
-- #### End of L10n settings ####
function getParameterValue(args, param_id, suffix)
suffix = suffix or ''
if type( L10n.para[param_id] ) ~= 'table' then
return args[L10n.para[param_id]..suffix]
end
for _i, paramAlias in ipairs(L10n.para[param_id]) do
if args[paramAlias..suffix] then
return args[paramAlias..suffix]
end
end
return nil
end
-- Trim whitespace from args, and remove empty args. Also fix control characters.
function trimArgs(argsTable)
local cleanArgs = {}
for key, val in pairs(argsTable) do
if type(val) == 'string' then
val = val:match('^%s*(.-)%s*$')
if val ~= '' then
-- control characters inside json need to be escaped, but stripping them is simpler
-- See also T214984
cleanArgs[key] = val:gsub('%c',' ')
end
else
cleanArgs[key] = val
end
end
return cleanArgs
end
function isAffirmed(val)
if not(val) then return false end
return string.find(L10n.str.affirmedWords, ' '..val..' ', 1, true ) and true or false
end
function isDeclined(val)
if not(val) then return false end
return string.find(L10n.str.declinedWords , ' '..val..' ', 1, true ) and true or false
end
local coordsDerivedFromFeatures = false;
function makeContent(args)
if getParameterValue(args, 'raw') then
coordsDerivedFromFeatures = true -- Kartographer should be able to automatically calculate coords from raw geoJSON
return getParameterValue(args, 'raw')
end
local content = {}
local argsExpanded = {}
for k, v in pairs(args) do
local index = string.match( k, '^[^0-9]+([0-9]*)$' )
if index ~= nil then
local indexNumber = ''
if index ~= '' then
indexNumber = tonumber(index)
else
indexNumber = 1
end
if argsExpanded[indexNumber] == nil then
argsExpanded[indexNumber] = {}
end
argsExpanded[indexNumber][ string.gsub(k, index, '') ] = v
end
end
for contentIndex, contentArgs in pairs(argsExpanded) do
-- Kartographer automatically calculates coords if geolines/shapes are used (T227402)
if not coordsDerivedFromFeatures then
local type = contentArgs['type']
coordsDerivedFromFeatures = ( type == L10n.str.line or type == L10n.str.shape ) and true or false
end
content[contentIndex] = makeContentJson(contentArgs)
end
--Single item, no array needed
if #content==1 then return content[1] end
--Multiple items get placed in a FeatureCollection
local contentArray = '[\n' .. table.concat( content, ',\n') .. '\n]'
return contentArray
end
function parseCoords(coords)
local parts = mw.text.split((mw.ustring.match(coords,'[_%.%d]+[NS][_%.%d]+[EW]') or ''), '_')
local lat_d = tonumber(parts[1])
local lat_m = tonumber(parts[2]) -- nil if coords are in decimal format
local lat_s = lat_m and tonumber(parts[3]) -- nil if coords are either in decimal format or degrees and minutes only
local lat = lat_d + (lat_m or 0)/60 + (lat_s or 0)/3600
if parts[#parts/2] == 'S' then
lat = lat * -1
end
local long_d = tonumber(parts[1+#parts/2])
local long_m = tonumber(parts[2+#parts/2]) -- nil if coords are in decimal format
local long_s = long_m and tonumber(parts[3+#parts/2]) -- nil if coords are either in decimal format or degrees and minutes only
local long = long_d + (long_m or 0)/60 + (long_s or 0)/3600
if parts[#parts] == 'W' then
long = long * -1
end
return lat, long
end
function wikidataCoords(item_id)
if not(mw.wikibase.isValidEntityId(item_id)) or not(mw.wikibase.entityExists(item_id)) then
error(L10n.error.noCoords, 0)
end
local coordStatements = mw.wikibase.getBestStatements(item_id, 'P625')
if not coordStatements or #coordStatements == 0 then
error(L10n.error.wikidataCoords, 0)
end
local hasNoValue = ( coordStatements[1].mainsnak and coordStatements[1].mainsnak.snaktype == 'novalue' )
if hasNoValue then
error(L10n.error.wikidataCoords, 0)
end
local wdCoords = coordStatements[1]['mainsnak']['datavalue']['value']
return tonumber(wdCoords['latitude']), tonumber(wdCoords['longitude'])
end
function makeCoords(args, plainOutput)
local coords, lat, long
local frame = mw.getCurrentFrame()
if getParameterValue(args, 'coord') then
coords = frame:preprocess( getParameterValue(args, 'coord') )
lat, long = parseCoords(coords)
else
lat, long = wikidataCoords(getParameterValue(args, 'id') or mw.wikibase.getEntityIdForCurrentPage())
end
if plainOutput then
return lat, long
end
return {[0] = long, [1] = lat}
end
function makeCircleCoords(args)
local lat, long = makeCoords(args, true)
local radius = getParameterValue(args, 'radius')
if not radius then
radius = getParameterValue(args, 'radiusKm') and tonumber(getParameterValue(args, 'radiusKm'))*1000
if not radius then
radius = getParameterValue(args, 'radiusMi') and tonumber(getParameterValue(args, 'radiusMi'))*1609.344
if not radius then
radius = getParameterValue(args, 'radiusFt') and tonumber(getParameterValue(args, 'radiusFt'))*0.3048
end
end
end
local edges = getParameterValue(args, 'edges') or L10n.defaults.edges
if not lat or not long then
error("Circle centre coordinates must be specified, or available via Wikidata")
elseif not radius then
error("Circle radius must be specified")
elseif tonumber(radius) <= 0 then
error("Circle radius must be a positive number")
elseif tonumber(edges) <= 0 then
error("Circle edges must be a positive number")
end
return circleToPolygon(lat, long, radius, tonumber(edges))
end
function circleToPolygon(lat, long, radius, n) -- n is number of edges
-- Based on https://github.com/gabzim/circle-to-polygon, ISC licence
function offset(cLat, cLon, distance, bearing)
local lat1 = math.rad(cLat)
local lon1 = math.rad(cLon)
local dByR = distance / 6378137 -- distance divided by 6378137 (radius of the earth) wgs84
local lat = math.asin(
math.sin(lat1) * math.cos(dByR) +
math.cos(lat1) * math.sin(dByR) * math.cos(bearing)
)
local lon = lon1 + math.atan2(
math.sin(bearing) * math.sin(dByR) * math.cos(lat1),
math.cos(dByR) - math.sin(lat1) * math.sin(lat)
)
return {math.deg(lon), math.deg(lat)}
end
local coordinates = {};
local i = 0;
while i < n do
table.insert(coordinates,
offset(lat, long, radius, (2*math.pi*i*-1)/n)
)
i = i + 1
end
table.insert(coordinates, offset(lat, long, radius, 0))
return coordinates
end
function makeContentJson(contentArgs)
local data = {}
if getParameterValue(contentArgs, 'type') == L10n.str.point or getParameterValue(contentArgs, 'type') == L10n.str.circle then
local isCircle = getParameterValue(contentArgs, 'type') == L10n.str.circle
data.type = "Feature"
data.geometry = {
type = isCircle and "LineString" or "Point",
coordinates = isCircle and makeCircleCoords(contentArgs) or makeCoords(contentArgs)
}
data.properties = {
title = getParameterValue(contentArgs, 'title') or mw.getCurrentFrame():getParent():getTitle()
}
if isCircle then
-- TODO: This is very similar to below, should be extracted into a function
data.properties.stroke = getParameterValue(contentArgs, 'strokeColor') or L10n.defaults.strokeColor
data.properties["stroke-width"] = tonumber(getParameterValue(contentArgs, 'strokeWidth')) or L10n.defaults.strokeWidth
local strokeOpacity = getParameterValue(contentArgs, 'strokeOpacity')
if strokeOpacity then
data.properties['stroke-opacity'] = tonumber(strokeOpacity)
end
local fill = getParameterValue(contentArgs, 'fill')
if fill then
data.properties.fill = fill
local fillOpacity = getParameterValue(contentArgs, 'fillOpacity')
data.properties['fill-opacity'] = fillOpacity and tonumber(fillOpacity) or 0.6
end
else -- is a point
data.properties["marker-symbol"] = getParameterValue(contentArgs, 'marker') or L10n.defaults.marker
data.properties["marker-color"] = getParameterValue(contentArgs, 'markerColor') or L10n.defaults.markerColor
data.properties["marker-size"] = getParameterValue(contentArgs, 'markerSize') or L10n.defaults.markerSize
end
else
data.type = "ExternalData"
if getParameterValue(contentArgs, 'type') == L10n.str.data or getParameterValue(contentArgs, 'from') then
data.service = "page"
elseif getParameterValue(contentArgs, 'type') == L10n.str.line then
data.service = "geoline"
elseif getParameterValue(contentArgs, 'type') == L10n.str.shape then
data.service = "geoshape"
elseif getParameterValue(contentArgs, 'type') == L10n.str.shapeInverse then
data.service = "geomask"
end
if getParameterValue(contentArgs, 'id') or (not (getParameterValue(contentArgs, 'from')) and mw.wikibase.getEntityIdForCurrentPage()) then
data.ids = getParameterValue(contentArgs, 'id') or mw.wikibase.getEntityIdForCurrentPage()
else
data.title = getParameterValue(contentArgs, 'from')
end
data.properties = {
stroke = getParameterValue(contentArgs, 'strokeColor') or L10n.defaults.strokeColor,
["stroke-width"] = tonumber(getParameterValue(contentArgs, 'strokeWidth')) or L10n.defaults.strokeWidth
}
local strokeOpacity = getParameterValue(contentArgs, 'strokeOpacity')
if strokeOpacity then
data.properties['stroke-opacity'] = tonumber(strokeOpacity)
end
local fill = getParameterValue(contentArgs, 'fill')
if fill and (data.service == "geoshape" or data.service == "geomask") then
data.properties.fill = fill
local fillOpacity = getParameterValue(contentArgs, 'fillOpacity')
if fillOpacity then
data.properties['fill-opacity'] = tonumber(fillOpacity)
end
end
end
data.properties.title = getParameterValue(contentArgs, 'title') or mw.getCurrentFrame():preprocess('{{PAGENAME}}')
if getParameterValue(contentArgs, 'description') then
data.properties.description = getParameterValue(contentArgs, 'description')
end
return mw.text.jsonEncode(data)
end
function makeTagAttribs(args, isTitle)
local attribs = {}
if getParameterValue(args, 'zoom') then
attribs.zoom = getParameterValue(args, 'zoom')
end
if isDeclined(getParameterValue(args, 'icon')) then
attribs.class = "no-icon"
end
if getParameterValue(args, 'type') == L10n.str.point and not coordsDerivedFromFeatures then
local lat, long = makeCoords(args, 'plainOutput')
attribs.latitude = tostring(lat)
attribs.longitude = tostring(long)
end
if isAffirmed(getParameterValue(args, 'frame')) and not(isTitle) then
attribs.width = getParameterValue(args, 'frameWidth') or L10n.defaults.frameWidth
attribs.height = getParameterValue(args, 'frameHeight') or L10n.defaults.frameHeight
if getParameterValue(args, 'frameCoordinates') then
local frameLat, frameLong = parseCoords(getParameterValue(args, 'frameCoordinates'))
attribs.latitude = frameLat
attribs.longitude = frameLong
else
if getParameterValue(args, 'frameLatitude') then
attribs.latitude = getParameterValue(args, 'frameLatitude')
end
if getParameterValue(args, 'frameLongitude') then
attribs.longitude = getParameterValue(args, 'frameLongitude')
end
end
if not attribs.latitude and not attribs.longitude and not coordsDerivedFromFeatures then
local success, lat, long = pcall(wikidataCoords, getParameterValue(args, 'id') or mw.wikibase.getEntityIdForCurrentPage())
if success then
attribs.latitude = tostring(lat)
attribs.longitude = tostring(long)
end
end
if getParameterValue(args, 'frameAlign') then
attribs.align = getParameterValue(args, 'frameAlign')
end
if isAffirmed(getParameterValue(args, 'plain')) then
attribs.frameless = "1"
else
attribs.text = getParameterValue(args, 'text') or L10n.defaults.text
end
else
attribs.text = getParameterValue(args, 'text') or L10n.defaults.text
end
return attribs
end
function makeTitleOutput(args, tagContent)
local titleTag = mw.text.tag('maplink', makeTagAttribs(args, true), tagContent)
local spanAttribs = {
style = "font-size: small;",
id = "coordinates"
}
return mw.text.tag('span', spanAttribs, titleTag)
end
function makeInlineOutput(args, tagContent)
local tagName = 'maplink'
if getParameterValue(args, 'frame') then
tagName = 'mapframe'
end
return mw.text.tag(tagName, makeTagAttribs(args), tagContent)
end
local p = {}
-- Entry point for templates
function p.main(frame)
local parent = frame.getParent(frame)
local output = p._main(parent.args)
return frame:preprocess(output)
end
-- Entry point for modules
function p._main(_args)
local args = trimArgs(_args)
local tagContent = makeContent(args)
local display = mw.text.split(getParameterValue(args, 'display') or L10n.defaults.display, '%s*' .. L10n.str.dsep .. '%s*')
local displayInTitle = display[1] == L10n.str.title or display[2] == L10n.str.title
local displayInline = display[1] == L10n.str.inline or display[2] == L10n.str.inline
local output
if displayInTitle and displayInline then
output = makeTitleOutput(args, tagContent) .. makeInlineOutput(args, tagContent)
elseif displayInTitle then
output = makeTitleOutput(args, tagContent)
elseif displayInline then
output = makeInlineOutput(args, tagContent)
else
error(L10n.error.badDisplayPara)
end
return output
end
return p