Modul:Location map
Aspect

Utilizare
Acest modul implementează formatele {{Location map}} și{{Location map~}}. Vă rugăm să vă uitați la paginile formatelor pentru instrucțiuni de utilizare.
Categorii de localizare/întreținere
local wd = require('Modul:Wikidata')
local p = {}
local getArgs = require('Modul:Arguments').getArgs
local function round(n, decimals)
local pow = 10^(decimals or 0)
return math.floor(n * pow + 0.5) / pow
end
local function wdQuery(qid, what)
local entity = mw.wikibase.getEntity(qid)
if what == "image" then
return wd.findOneValueNoRef("P242", qid)
elseif what == "image1" then
return wd.findOneValueNoRef("P1944", qid)
elseif what == "top" then
if entity and entity.claims and entity.claims.P1332 and entity.claims.P1332[1].mainsnak.snaktype == 'value' then
local value = entity.claims.P1332[1].mainsnak.datavalue.value
return value.latitude
end
elseif what == "bottom" then
if entity and entity.claims and entity.claims.P1333 and entity.claims.P1333[1].mainsnak.snaktype == 'value' then
local value = entity.claims.P1333[1].mainsnak.datavalue.value
return value.latitude
end
elseif what == "right" then
if entity and entity.claims and entity.claims.P1334 and entity.claims.P1334[1].mainsnak.snaktype == 'value' then
local value = entity.claims.P1334[1].mainsnak.datavalue.value
return value.longitude
end
elseif what == "left" then
if entity and entity.claims and entity.claims.P1335 and entity.claims.P1335[1].mainsnak.snaktype == 'value' then
local value = entity.claims.P1335[1].mainsnak.datavalue.value
return value.longitude
end
else
return wd.findLabel(qid)
end
end
-- go up the P131 hyerarchy until we find an item with geo limits
-- if none is found, try skipping to the country
local function getQidWithGeoLimits(qOrig, depth)
if depth > 3 then return nil end
local entity = mw.wikibase.getEntity(qOrig)
local qFin = nil
-- we're going to trust WD here and assume all limits exist if one exists
if entity and entity.claims and entity.claims.P1332 then return qOrig end
if entity and entity.claims and entity.claims.P131 then
qFin = getQidWithGeoLimits(entity.claims.P131[1].mainsnak.datavalue.value.id, depth+1)
if qFin ~= nil then return qFin end
end
if depth == 0 and entity and entity.claims and entity.claims.P17 then
qFin = getQidWithGeoLimits(entity.claims.P17[1].mainsnak.datavalue.value.id, depth+1)
end
return qFin
end
function p.getMapParams(map, frame, args)
if not args then
args = getArgs(frame, {frameOnly = true})
end
if not map and not args.useWikidata then
error('The name of the location map definition to use must be specified', 2)
end
local moduletitle = mw.title.makeTitle('Modul', 'Location map/data/' .. map)
if not moduletitle then
error(string.format('%q is not a valid name for a location map definition', map), 2)
elseif moduletitle.exists then
local mapData = mw.loadData('Modul:Location map/data/' .. map)
return function(name, params)
if name == nil then
return 'Modul:Location map/data/' .. map
elseif mapData[name] == nil then
return ''
elseif params then
return mw.message.newRawMessage(tostring(mapData[name]), unpack(params)):plain()
else
return mapData[name]
end
end
elseif mw.title.makeTitle('Format', 'Harta de localizare ' .. map).exists then
local cache = {}
if type(frame) ~= 'table' or type(frame.expandTemplate) ~= 'function' then
--mw.logObject(frame)
error('A frame must be provided when using a legacy location map')
end
return function(name, params)
if params then
return frame:expandTemplate{title = 'Harta de localizare ' .. map, args = { name, unpack(params) }}
else
if name == nil then
return 'Format:Harta de localizare ' .. map
elseif cache[name] == nil then
cache[name] = frame:expandTemplate{title = 'Harta de localizare ' .. map, args = { name }}
end
return cache[name]
end
end
-- no map, try using Wikidata?
elseif args.useWikidata then
local cache = {}
cache["qid"] = getQidWithGeoLimits(args.qid, 0) or args.qid
return function(name, params)
if name == nil then name = "name" end
if cache[name] == nil then
cache[name] = wdQuery(cache["qid"], name)
elseif params then
return mw.message.newRawMessage(tostring(cache[name]), unpack(params)):plain()
end
return cache[name]
end
else
error('Unable to find the specified location map definition. Neither "Modul:Location map/data/' .. map .. '" nor "Format:Harta de localizare ' .. map .. '" exists', 2)
end
end
function p.data(frame, args, map)
if not args then
args = getArgs(frame, {frameOnly = true})
end
if not map then
map = p.getMapParams(args[1], frame, args)
end
local params = {}
for k,v in ipairs(args) do
if k > 2 then
params[k-2] = v
end
end
return map(args[2], #params ~= 0 and params)
end
local hemisphereMultipliers = {
longitude = { W = -1, w = -1, E = 1, e = 1, V = -1, v = -1 },
latitude = { S = -1, s = -1, N = 1, n = 1 }
}
local function decdeg(degrees, minutes, seconds, hemisphere, decimal, direction)
if decimal then
if degrees then
error('Decimal and DMS degrees cannot both be provided for ' .. direction, 2)
elseif minutes then
error('Minutes can only be provided with DMS degrees for ' .. direction, 2)
elseif seconds then
error('Seconds can only be provided with DMS degrees for ' .. direction, 2)
elseif hemisphere then
error('A hemisphere can only be provided with DMS degrees for ' .. direction, 2)
end
local retval = tonumber(decimal)
if retval then
return retval
end
error('The value "' .. decimal .. '" provided for ' .. direction .. ' is not valid', 2)
elseif seconds and not minutes then
error('Seconds were provided for ' .. direction .. ' without minutes also being provided', 2)
elseif not degrees then
if minutes then
error('Minutes were provided for ' .. direction .. ' without degrees also being provided', 2)
elseif hemisphere then
error('A hemisphere was provided for ' .. direction .. ' without degrees also being provided', 2)
end
return nil
end
local lang = mw.language.getContentLanguage()
if not tonumber(degrees) then
degrees = lang:parseFormattedNumber(degrees)
end
if not tonumber(minutes) then
minutes = lang:parseFormattedNumber(minutes)
end
if not tonumber(seconds) then
seconds = lang:parseFormattedNumber(seconds)
end
--[[mw.log("Modified coordinate")
mw.logObject(degrees)
mw.logObject(minutes)
mw.logObject(seconds)]]--
decimal = tonumber(degrees)
if not decimal then
error('Valoarea pentru grade "' .. degrees .. '" oferită pentru ' .. direction .. ' nu este validă', 2)
elseif minutes and not tonumber(minutes) then
error('Valoarea pentru minute "' .. minutes .. '" oferită pentru ' .. direction .. ' nu este validă', 2)
elseif seconds and not tonumber(seconds) then
error('Valoarea pentru secunde "' .. seconds .. '" oferită pentru ' .. direction .. ' nu este validă', 2)
end
decimal = decimal + (minutes or 0)/60 + (seconds or 0)/3600
if hemisphere then
local multiplier = hemisphereMultipliers[direction][hemisphere]
if not multiplier then
error('Emisfera "' .. hemisphere .. '" oferită pentru ' .. direction .. ' nu este validă', 2)
end
decimal = decimal * multiplier
end
return decimal
end
-- effectively make removeBlanks false for caption and maplink, and true for everything else
-- if useWikidata is present but blank, convert it to false instead of nil
-- p.top, p.bottom, and their callers need to use this
function p.valueFunc(key, value)
if value then
value = mw.text.trim(value)
end
if value ~= '' or key == 'caption' or key == 'maplink' then
return value
elseif key == 'useWikidata' then
return false
end
end
local function getContainerImage(args, map)
if args.AlternativeMap then
return args.AlternativeMap
elseif args.relief and map('image1') ~= '' then
return map('image1')
else
return map('image')
end
end
local function getDecimalCoords(args, map)
local longitude, latitude
longitude = decdeg(args.lon_deg, args.lon_min, args.lon_sec, args.lon_dir, args.long, 'longitude')
latitude = decdeg(args.lat_deg, args.lat_min, args.lat_sec, args.lat_dir, args.lat, 'latitude')
if not longitude and not latitude and args.useWikidata then
-- If they didn't provide either coordinate, try Wikidata. If they provided one but not the other, don't.
local entity = mw.wikibase.getEntity(args.qid)
if entity and entity.claims and entity.claims.P625 and entity.claims.P625[1].mainsnak.snaktype == 'value' then
local value = entity.claims.P625[1].mainsnak.datavalue.value
longitude, latitude = value.longitude, value.latitude
end
end
if not longitude then
error('No value was provided for longitude')
end
if not latitude then
error('No value was provided for latitude')
end
return latitude, longitude
end
--get map aspect ratio
local function getWidth(map, args)
local width
if not args.width then
width = round((args.default_width or 240) * (tonumber(map('defaultscale')) or 1))
elseif mw.ustring.sub(args.width, -2) == 'px' then
width = mw.ustring.sub(args.width, 1, -3)
else
width = args.width
end
return tonumber(width)
end
--distance logic adapted from https://wiki.openstreetmap.org/wiki/Zoom_levels#Distance_per_pixel_math
local function getHeight(map, latitude, longitude, width)
local bottom = tonumber(map('bottom'))
local top = tonumber(map('top'))
local left = tonumber(map('left'))
local right = tonumber(map('right'))
if not bottom or not top or not left or not right then return width end
local c = math.cos(math.rad(latitude)) -- variation of height in degrees
local deg_len = 111120 -- length of a degree in many at Equator and on meridians
local x_distance = (right - left) * deg_len * c
local y_distance = (top - bottom) * deg_len
local height = math.ceil(width * y_distance / x_distance)
return height
end
-- this function returns the maximum zoom that contains the whole map
-- calculations are based on data from https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames
local function getZoomAndCenter(map, latitude, longitude, width, height)
local bottom = tonumber(map('bottom'))
local top = tonumber(map('top'))
local left = tonumber(map('left'))
local right = tonumber(map('right'))
if not bottom or not top or not left or not right then
return 0, latitude, longitude --zoom 0 is the whole world
end
local tile_width = 256 -- OSM constant
local c = math.cos(math.rad(latitude)) -- variation of height in degrees
local x_tiles = 1.0 * width / tile_width
local y_tiles = 1.0 * height / tile_width
local x_zoom = math.floor(math.log(x_tiles * 360 / (right - left)) / math.log(2))
local y_zoom = math.floor(math.log(y_tiles * 360 * c / (top - bottom)) / math.log(2))
local x = (right + left) / 2
local y = (top + bottom) / 2
return math.min(x_zoom, y_zoom), y, x
end
function p.top(frame, args, map)
if not args then
args = getArgs(frame, {frameOnly = true, valueFunc = p.valueFunc})
end
if not map then
map = p.getMapParams(args[1], frame, args)
end
local width = getWidth(map, args)
local retval = args.float == 'center' and '<div class="center">' or ''
if args.caption and args.caption ~= '' and args.border ~= 'infobox' then
retval = retval .. '<div class="noviewer thumb '
if args.float == '"left"' or args.float == 'left' then
retval = retval .. 'tleft'
elseif args.float == '"center"' or args.float == 'center' or args.float == '"none"' or args.float == 'none' then
retval = retval .. 'tnone'
else
retval = retval .. 'tright'
end
retval = retval .. '"><div class="thumbinner" style="width:' .. (width + 2) .. 'px'
if args.border == 'none' then
retval = retval .. ';border:none'
elseif args.border then
retval = retval .. ';border-color:' .. args.border
end
retval = retval .. '"><div style="position:relative;width:' .. width .. 'px' .. (args.border ~= 'none' and ';border:1px solid lightgray">' or '">')
else
retval = retval .. '<div style="width:' .. width .. 'px;'
if args.float == '"left"' or args.float == 'left' then
retval = retval .. 'float:left;clear:left'
elseif args.float == '"center"' or args.float == 'center' then
retval = retval .. 'float:none;clear:both;margin-left:auto;margin-right:auto'
elseif args.float == '"none"' or args.float == 'none' then
retval = retval .. 'float:none;clear:none'
else
retval = retval .. 'float:right;clear:right'
end
retval = retval .. '"><div style="width:' .. width .. 'px;padding:0"><div style="position:relative;width:' .. width .. 'px">'
end
local image = getContainerImage(args, map)
if not image then error('Nu am putut găsi imaginea hărții pentru ' .. args[1]) end
retval = string.format(
'%s[[File:%s|%spx|%s%s]]',
retval,
image,
width,
args.alt or ((args.label or mw.title.getCurrentTitle().text) .. ' se află în ' .. map('name')),
args.maplink and ('|link=' .. args.maplink) or ''
)
if args.overlay_image then
return retval .. '<div style="position:absolute;top:0;left:0">[[File:' .. args.overlay_image .. '|' .. width .. 'px]]</div>'
else
return retval
end
end
function p.bottom(frame, args, map)
if not args then
args = getArgs(frame, {frameOnly = true, valueFunc = p.valueFunc})
end
if not map then
map = p.getMapParams(args[1], frame, args)
end
local retval = '</div>'
if not args.caption or args.border == 'infobox' then
if args.border then
retval = retval .. '<div>'
else
retval = retval .. '<div style="font-size:90%;padding-top:3px">'
end
retval = retval
.. (args.caption or (args.label or mw.title.getCurrentTitle().text) .. ' (' .. map('name') .. ')')
.. '</div>'
elseif args.caption ~= '' then
-- This is not the pipe trick. We're creating a link with no text on purpose, so that CSS can give us a nice image
retval = retval .. '<div class="thumbcaption"><div class="magnify">[[:File:' .. getContainerImage(args, map) .. '| ]]</div>' .. args.caption .. '</div>'
end
if args.switcherLabel then
retval = retval .. '<span class="switcher-label" style="display:none">' .. args.switcherLabel .. '</span>'
elseif args.autoSwitcherLabel then
retval = retval .. '<span class="switcher-label" style="display:none">Show map of ' .. map('name') .. '</span>'
end
retval = retval .. '</div></div>'
if args.caption_undefined then
--mw.log('Removed parameter caption_undefined used.')
local parent = frame:getParent()
if parent then
--mw.log('Parent is ' .. parent:getTitle())
end
--mw.logObject(args, 'args')
retval = retval .. '[[Categorie:Hartă de localizare cu posibile erori|Page using removed parameter]]'
end
if map('skew') ~= '' or map('lat_skew') ~= '' or map('crosses180') ~= '' or map('type') ~= '' then
--mw.log('Removed parameter used in map definition ' .. map())
retval = retval .. '[[Categorie:Hartă de localizare cu posibile erori|Map using removed parameter]]'
end
if string.find(map('name'), '|', 1, true) then
--mw.log('Pipe used in name of map definition ' .. map())
retval = retval .. '[[Category:Hartă de localizare cu posibile erori|Name containing pipe]]'
end
if args.float == 'center' then
retval = retval .. '</div>'
end
return retval
end
local function markOuterDiv(x, y, imageDiv, labelDiv)
return mw.html.create('div')
:cssText('position:absolute;top:' .. round(y, 3) .. '%;left:' .. round(x, 3) .. '%')
:node(imageDiv)
:node(labelDiv)
end
local function markImageDiv(mark, marksize, label, link, alt, title)
local builder = mw.html.create('div')
:cssText('position:absolute;left:-' .. round(marksize / 2) .. 'px;top:-' .. round(marksize / 2) .. 'px;line-height:0')
:attr('title', title)
if marksize ~= 0 then
builder:wikitext(string.format(
'[[File:%s|%dx%dpx|%s|link=%s%s]]',
mark,
marksize,
marksize,
label,
link,
alt and ('|alt=' .. alt) or ''
))
end
return builder
end
local function markLabelDiv(label, label_size, label_width, position, background, x, marksize)
if tonumber(label_size) == 0 then
return mw.html.create('div'):cssText('font-size:0%;position:absolute'):wikitext(label)
end
local builder = mw.html.create('div')
:cssText('font-size:' .. label_size .. '%;line-height:110%;position:absolute;width:' .. label_width .. 'em')
local distance = round(marksize / 2 + 1)
local spanCss
if position == 'top' then -- specified top
builder:cssText('bottom:' .. distance .. 'px;left:' .. (-label_width / 2) .. 'em;text-align:center')
elseif position == 'bottom' then -- specified bottom
builder:cssText('top:' .. distance .. 'px;left:' .. (-label_width / 2) .. 'em;text-align:center')
elseif position == 'left' or (tonumber(x) > 70 and position ~= 'right') then -- specified left or autodetected to left
builder:cssText('top:-0.75em;right:' .. distance .. 'px;text-align:right')
spanCss = 'float:right'
else -- specified right or autodetected to right
builder:cssText('top:-0.75em;left:' .. distance .. 'px;text-align:left')
spanCss = 'float:left'
end
builder = builder:tag('span')
:cssText('padding:1px')
:cssText(spanCss)
:wikitext(label)
if background then
builder:cssText('background-color:' .. background)
end
return builder:done()
end
local function getX(longitude, left, right)
local width = (right - left) % 360
if width == 0 then
width = 360
end
local distanceFromLeft = (longitude - left) % 360
-- the distance needed past the map to the right equals distanceFromLeft - width. the distance needed past the map to the left equals 360 - distanceFromLeft. to minimize page stretching, go whichever way is shorter
if distanceFromLeft - width / 2 >= 180 then
distanceFromLeft = distanceFromLeft - 360
end
return 100 * distanceFromLeft / width
end
local function getY(latitude, top, bottom)
return 100 * (top - latitude) / (top - bottom)
end
function p.mark(frame, args, map)
if not args then
args = getArgs(frame, {wrappers = {'Format:Harta de localizare~', 'Format:Location map~'}})
end
if not map then
map = p.getMapParams(args[1], frame, args)
end
local x, y
local latitude, longitude = getDecimalCoords(args, map)
local builder = mw.html.create()
if (not args.lon_deg) ~= (not args.lat_deg) then
builder:wikitext('[[Categorie:Hartă de localizare cu precizii diferite ale longitudinii și latitudinii|Degrees]]')
elseif (not args.lon_min) ~= (not args.lat_min) then
builder:wikitext('[[Categorie:Hartă de localizare cu precizii diferite ale longitudinii și latitudinii|Minutes]]')
elseif (not args.lon_sec) ~= (not args.lat_sec) then
builder:wikitext('[[Categorie:Hartă de localizare cu precizii diferite ale longitudinii și latitudinii|Seconds]]')
elseif (not args.lon_dir) ~= (not args.lat_dir) then
builder:wikitext('[[Categorie:Hartă de localizare cu precizii diferite ale longitudinii și latitudinii|Emisferă]]')
elseif (not args.long) ~= (not args.lat) then
builder:wikitext('[[Categorie:Hartă de localizare cu precizii diferite ale longitudinii și latitudinii|Zecimal]]')
end
if args.skew or args.lon_shift or args.markhigh then
--mw.log('Removed parameter used in invocation.')
local parent = frame:getParent()
if parent then
--mw.log('Parent is ' .. parent:getTitle())
end
--mw.logObject(args, 'args')
builder:wikitext('[[Categorie:Hartă de localizare cu posibile erori|Page using removed parameter]]')
end
if map('x') ~= '' then
x = tonumber(mw.ext.ParserFunctions.expr(map('x', { latitude, longitude })))
else
x = tonumber(getX(longitude, map('left'), map('right')))
end
if map('y') ~= '' then
y = tonumber(mw.ext.ParserFunctions.expr(map('y', { latitude, longitude })))
else
y = tonumber(getY(latitude, map('top'), map('bottom')))
end
if (x < 0 or x > 100 or y < 0 or y > 100) and not args.outside then
--mw.log('Mark placed outside map boundaries without outside flag set. x = ' .. x .. ', y = ' .. y)
local parent = frame:getParent()
if parent then
--mw.log('Parent is ' .. parent:getTitle())
end
--mw.logObject(args, 'args')
builder:wikitext('[[Categorie:Hartă de localizare cu posibile erori|Outside flag not set with mark outside map]]')
end
local mark = args.mark or map('mark')
if mark == '' then
mark = 'Red pog.svg'
end
local marksize = tonumber(args.marksize) or tonumber(map('marksize')) or 8
local imageDiv = markImageDiv(mark, marksize, args.label or mw.title.getCurrentTitle().text, args.link or '', args.alt, args[2])
local labelDiv
if args.label and args.position ~= 'none' then
labelDiv = markLabelDiv(args.label, args.label_size or 90, args.label_width or 6, args.position, args.background, x, marksize)
end
return builder:node(markOuterDiv(x, y, imageDiv, labelDiv))
end
local function getMarkSize(args, map)
local marksize = tonumber(args.marksize) or tonumber(map('marksize')) or 8
if marksize < 8 then return 'small' end
if marksize > 10 then return 'large' end
return 'medium'
end
-- the current version only allows for a single point value, so we hardcode it
function p.mapframe(frame, args, map)
if not args then
args = getArgs(frame, {frameOnly = true, valueFunc = p.valueFunc})
end
if not map then
map = p.getMapParams(args[1], frame, args)
end
local width = getWidth(map, args)
local latitude, longitude = getDecimalCoords(args, map)
local height = getHeight(map, latitude, longitude, width)
local zoom, clat, clon = getZoomAndCenter(map, latitude, longitude, width, height)
local align = args.float
if not align or args.float == '"none"' or args.float == 'none' then align = "center" end
local result = string.format([[<mapframe text="" width=%d height=%d zoom=%d latitude=%f longitude=%f align="%s">
[{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [%f, %f] },
"properties": {
"title": "%s",
"marker-symbol": "%s",
"marker-size": "%s",
"marker-color": "0050d0"
}
},
{
"type": "ExternalData",
"service": "geoshape",
"ids": "%s",
}
]
</mapframe>]],
width, height, zoom, clat, clon, align,
longitude, latitude,
args.caption or (args.label or mw.title.getCurrentTitle().text) .. ' (' .. map('name') .. ')',
args.mark or 'marker',
getMarkSize(args, map),
args.qid or mw.wikibase.getEntityIdForCurrentPage() or "Q218")
return frame:preprocess(result)
end
function p.main(frame, args, map)
if not args then
args = getArgs(frame, {wrappers = 'Format:Harta de localizare', valueFunc = p.valueFunc})
end
if args.useWikidata == nil then
args.useWikidata = true
end
if not map then
if args[1] then
map = {}
for mapname in string.gmatch(args[1], '[^#]+') do
map[#map + 1] = p.getMapParams(mapname, frame, args)
end
if #map == 1 then map = map[1] end
elseif args.useWikidata then
map = p.getMapParams('wikidata', frame, args)
else
map = p.getMapParams('World', frame, args)
end
end
if type(map) == 'table' then
local outputs = {}
args.autoSwitcherLabel = true
for k,v in ipairs(map) do
outputs[k] = p.main(frame, args, v)
end
return '<div class="switcher-container">' .. table.concat(outputs) .. '</div>'
else
if args.mapframe == "1" then
return p.mapframe(frame, args, map) .. p.bottom(frame, args, map)
else
return p.top(frame, args, map) .. tostring( p.mark(frame, args, map) ) .. p.bottom(frame, args, map)
end
end
end
return p