A documentação para este módulo pode ser criada em Módulo:Map draw/doc
-- Credits: -- Original from Wikivoyage -- Developed for Kartographer version on Wikipedia by Vriullop @cawiki -- Formulae: -- CSGNetwork at http://www.csgnetwork.com/degreelenllavcalc.html via @enwiki -- OpenStreetMap -- Version: 20200509 local p = {} -- Localization on [[Module:Map/i18n]] local i18n = { ["coordinate-invalid"] = "Parameter $1 is an invalid value of \"latitude,longitude\".", ["type-invalid"] = "Type $1 is invalid. Use mapframe or maplink.", ["geotype-invalid"] = "Geotype $1 is an invalid value.", ["ids-invalid"] = "Parameter ids $1 is invalid.", ["polygon-required-points"] = "A polygon requires a minimum of 4 coordinate points.", ["polygon-not-closed"] = "A closed polygon requires last point equal to first one.", ['ids-not-found'] = "Ids not found for external data.", --['not-from-content-page'] = "Do not invoke from content page. Use a template or use a module subpage like /sandbox for testing .", -- local categories ['cat-several-features'] = "", ['cat-linestring-drawn'] = "", ['cat-polygon-drawn'] = "", } local cat = {['cat-several-features'] = false, ['cat-linestring-drawn'] = false, ['cat-polygon-drawn'] = false} -- Credit to http://stackoverflow.com/a/1283608/2644759, cc-by-sa 3.0 local function tableMerge(t1, t2) for k, v in pairs(t2) do if type(v) == "table" then if type(t1[k] or false) == "table" then tableMerge(t1[k] or {}, t2[k] or {}) else t1[k] = v end else t1[k] = v end end return t1 end local function loadI18n() local exist, res = pcall(require, "Module:Map/i18n") if exist and next(res) ~= nil then tableMerge(i18n, res.i18n) end end loadI18n() local errormessage local function printError(key, par) -- just print first error errormessage = errormessage or ('<span class="error">' .. (par and mw.ustring.gsub(i18n[key], "$1", par) or i18n[key]) .. '</span>') end -- Convert coordinates input format to geojson table local function parseGeoSequence(data, geotype) local coordsGeo = {} for line_coord in mw.text.gsplit(data, ':', true) do -- Polygon - linearRing:linearRing... local coordsLine = {} for point_coord in mw.text.gsplit(line_coord, ';', true) do -- LineString or MultiPoint - point;point... local valid = false local val = mw.text.split(point_coord, ',', true) -- Point - lat,lon -- allow for elevation if #val >= 2 and #val <= 3 then local lat = tonumber(val[1]) local lon = tonumber(val[2]) if lat ~= nil and lon ~= nil then table.insert(coordsLine, {lon, lat}) valid = true end end if not valid and point_coord ~= '' then printError('coordinate-invalid', point_coord) end end if geotype == 'Polygon' then if #coordsLine < 4 then printError('polygon-required-points') elseif table.concat(coordsLine[1]) ~= table.concat(coordsLine[#coordsLine]) then printError('polygon-not-closed') end end table.insert(coordsGeo, coordsLine) end if geotype == 'Point' then coordsGeo = coordsGeo[1][1] elseif geotype == "LineString" or geotype == "MultiPoint" then coordsGeo = coordsGeo[1] elseif geotype ~= 'Polygon' then printError('geotype-invalid', geotype) end return coordsGeo end -- data Point - {lon,lat} -- data LineString - { {lon,lat}, {lon,lat}, ... } -- data Polygon - { { {lon,lat}, {lon,lat} }, { {lon,lat}, {lon,lat} }, ... } -- output as LineString format local function mergePoints(stack, merger) if merger == nil then return stack end for _, val in ipairs(merger) do if type(val) == "number" then -- Point format stack[#stack + 1] = merger break elseif type(val[1]) == "table" then -- Polygon format for _, val2 in ipairs(val) do stack[#stack + 1] = val2 end else -- LineString format stack[#stack + 1] = val end end return stack end -- remove duplicated points, they may affect zoom calculation local function setUniquePoints(t) -- build set of unique values local uniqueElements = {} for _, point in ipairs(t) do if not uniqueElements[point[1]] then uniqueElements[point[1]] = {} end uniqueElements[point[1]][point[2]] = true end -- convert the set local result = {} for lon, _ in pairs(uniqueElements) do for lat, _ in pairs(uniqueElements[lon]) do table.insert(result, {lon, lat}) end end return result end local function getCoordBounds(data) local latN, latS = -90, 90 local lonE, lonW = -180, 180 for i, val in ipairs(data) do latN = math.max(val[2], latN) latS = math.min(val[2], latS) lonE = math.max(val[1], lonE) lonW = math.min(val[1], lonW) end return latN, latS, lonE, lonW end local function getCoordCenter(data) local latN, latS, lonE, lonW = getCoordBounds(data) local latCenter = latS + (latN - latS) / 2 local lonCenter = lonW + (lonE - lonW) / 2 return lonCenter, latCenter end -- meters per degree by latitude local function mxdByLat(lat) local latRad = math.rad(lat) -- see [[Geographic coordinate system#Expressing latitude and longitude as linear units]], by CSGNetwork local mxdLat = 111132.92 - 559.82 * math.cos(2 * latRad) + 1.175 * math.cos(4 * latRad) - 0.023 * math.cos(6 * latRad) local mxdLon = 111412.84 * math.cos(latRad) - 93.5 * math.cos(3 * latRad) + 0.118 * math.cos(5 * latRad) return mxdLat, mxdLon end -- Calculate zoom to fit coordinate bounds into height and width of frame local function getZoom(data, height, width) local lat1, lat2, lon1, lon2 = getCoordBounds(data) local latMid = (lat1 + lat2) / 2 -- mid latitude local mxdLat, mxdLon = mxdByLat(latMid) -- distances in meters local distLat = math.abs((lat1 - lat2) * mxdLat) local distLon = math.abs((lon1 - lon2) * mxdLon) -- margin 100px in height and width, right upper icon is about 50x50px local validHeight = math.max(height - 100, 100) local validWidth = math.max(width - 100, 100) -- maximum zoom fitting all points local latRad = math.rad(latMid) for zoom = 19, 0, -1 do -- see https://wiki.openstreetmap.org/wiki/Zoom_levels#Metres_per_pixel_math -- equatorial circumference 40 075 036 m: [[Equator#Exact length]] local distLatFrame = 40075036 * validHeight * math.cos(latRad) / (2 ^ (zoom + 8)) local distLonFrame = 40075036 * validWidth * math.cos(latRad) / (2 ^ (zoom + 8)) if distLatFrame > distLat and distLonFrame > distLon then return zoom end end return 0 end -- Geotype based on coordinates format pattern local function findGeotype(coord) local _, semicolons = string.gsub(coord, ';', '') local firstcoord = string.match(coord, "[0-9%.%-]+%s*,%s*[0-9%.%-]+") local lastcoord = string.match(string.reverse(coord), "[0-9%.%-]+%s*,%s*[0-9%.%-]+") if firstcoord == nil or lastcoord == nil then printError('coordinate-invalid', coord) else lastcoord = string.reverse(lastcoord) end if string.find(coord, ':') or (semicolons > 2 and firstcoord == lastcoord) then return 'Polygon' elseif semicolons > 0 then return 'LineString' -- or MultiPoint else return 'Point' end end local function fetchWikidata(id, snak) -- snak is a table like {'claims', 'P625', 1, 'mainsnak', 'datavalue', 'value'} local value id = mw.text.trim(id) if not string.find(id, "^Q%d+$") then printError('ids-invalid', id) else value = mw.wikibase.getBestStatements(id, snak[2]) for i = 3, #snak do if value == nil then break end value = value[snak[i]] end end return value end -- Fetch coordinates from Wikidata for a list of comma separated ids local function getCoordinatesById(ids) local function roundPrec(num, prec) if prec == nil or prec <= 0 then return num end local sig = 10^math.floor(math.log10(prec)+.5) -- significant figure from sexagesimal precision: 0.00123 -> 0.001 return math.floor(num / sig + 0.5) * sig end if ids == nil then return end local coord = {} local snak = {'claims', 'P625', 1, 'mainsnak', 'datavalue', 'value'} for idx in mw.text.gsplit(ids, '%s*,%s*') do local value = fetchWikidata(idx, snak) if value then local prec = value.precision coord[#coord+1] = roundPrec(value.latitude, prec) .. ',' .. roundPrec(value.longitude, prec) end end return #coord > 0 and table.concat(coord, ';') or nil end local function getBoundsById(ids, coordInput) if ids == nil then return {} end local coord = mw.text.split(coordInput, '%s*;%s*') local id = mw.text.split(ids, '%s*,%s*') if #coord ~= #id then return {} end local id_parent = nil if #id == 1 then id_parent = fetchWikidata(id[1], {'claims', 'P131', 1, 'mainsnak', 'datavalue', 'value', 'id'}) if id_parent ~= nil then id[2] = id_parent -- P131: located in the administrative territorial entity, last try coord[2] = coord[1] end end local bounds = {} -- try to fetch Wikidata in this order: area, watershed area, population, and finally by administrative entity local snak_area = {'claims', 'P2046', 1, 'mainsnak', 'datavalue', 'value'} -- area and unit local snak_warea = {'claims', 'P2053', 1, 'mainsnak', 'datavalue', 'value'} -- area and unit local snak_pop = {'claims', 'P1082', 1, 'mainsnak', 'datavalue', 'value'} -- population local convert_area = {['Q712226'] = 1000000, ['Q35852'] = 10000, ['Q232291'] = 2589988.110336, ['Q81292'] = 4046.8564224, ['Q935614'] = 1600, ['Q857027'] = 0.09290304, ['Q21074767'] = 1138100, ['Q25343'] = 1} -- to square metres -- query Wikidata: http://tinyurl.com/j8aez2g for i = 1, #id do if i == 2 and id[2] == id_parent and #bounds > 0 then break end -- only if not found previously local amount, unit, area local value = fetchWikidata(id[i], snak_area) or fetchWikidata(id[i], snak_warea) if value then amount = tonumber(value.amount) unit = string.match(value.unit, "(Q%d+)") if convert_area[unit] then area = amount * convert_area[unit] end end if area == nil then value = fetchWikidata(id[i], snak_pop) if value then amount = tonumber(value.amount) -- average density estimated for populated areas: 100; see [[Population density]] area = amount / 100 * 1000000 end end if area then local radius = math.sqrt(area / math.pi) -- approximation with a circle local latlon = mw.text.split(coord[i], '%s*,%s*') local mxdLat, mxdLon = mxdByLat(latlon[1]) bounds[#bounds+1] = {latlon[2] + (radius / mxdLon), latlon[1] + (radius / mxdLat)} -- NE bound, geoJSON format bounds[#bounds+1] = {latlon[2] - (radius / mxdLon), latlon[1] - (radius / mxdLat)} -- SW bound end end return bounds end local function circleToPolygon(center, radius, edges, turn) -- From en:Module:Mapframe, based on https://github.com/gabzim/circle-to-polygon, ISC licence local 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(lat) .. ',' .. math.deg(lon) end local coords = mw.text.split(center, ',', true) local lat = tonumber(coords[1]) local long = tonumber(coords[2]) edges = edges or 32 local move = 2 * math.pi * (turn or 0) local coordinates = {} for i = 0, edges do table.insert(coordinates, offset(lat, long, radius, ((2*math.pi*-i)/edges) + move)) end return table.concat(coordinates, ';') end local function addCategories(geotype, i) if not mw.title.getCurrentTitle().isContentPage then return end if i > 2 and i18n["cat-several-features"] ~= '' then cat["cat-several-features"] = true end if geotype == "LineString" and i18n["cat-linestring-drawn"] ~= '' then cat["cat-linestring-drawn"] = true elseif geotype == "Polygon" and i18n["cat-polygon-drawn"] ~= '' then cat["cat-polygon-drawn"] = true end return end -- Main function function p._tag(args) local tagname = args.type or 'mapframe' if tagname ~= 'maplink' and tagname ~= 'mapframe' then printError('type-invalid', tagname) end local tagArgs = { text = args.text, zoom = tonumber(args.zoom), latitude = tonumber(args.latitude), longitude = tonumber(args.longitude) } local defaultzoom = tonumber(args.default_zoom) if tagname == 'mapframe' then tagArgs.width = args.width or 300 tagArgs.height = args.height or 300 tagArgs.align = args.align or 'right' if args.frameless ~= nil and tagArgs.text == nil then tagArgs.frameless = true end else tagArgs.class = args.class end local wdid = args.item or mw.wikibase.getEntityIdForCurrentPage() if args['coordinates1'] == nil and args['geotype1'] == nil then -- single feature args['coordinates1'] = args['coordinates'] or args[1] if args['coordinates1'] == nil and args['latitude'] and args['longitude'] then args['coordinates1'] = args['latitude'] .. ',' .. args['longitude'] elseif args['coordinates1'] == nil then args['coordinates1'] = getCoordinatesById(wdid) end local par = {'title', 'image', 'description', 'geotype', 'commons', 'radius', 'radiuskm', 'edges', 'turn'} for _, v in ipairs(par) do args[v .. '1'] = args[v .. '1'] or args[v] end end local externalData = {['geoshape'] = true, ['geomask'] = true, ['geoline'] = true, ['page'] = true, ['none'] = true} local featureCollection = {['Point'] = true, ['MultiPoint'] = true, ['LineString'] = true, ['Polygon'] = true, ['circle'] = true} local myfeatures, myexternal, allpoints = {}, {}, {} local i, j = 1, 1 while args['coordinates'..i] or args['ids'..i] or externalData[args['geotype'..i]] or args['commons'..i] do local geotypex = args['geotype'..i] or args['geotype'] if geotypex == nil and args['commons'..i] then geotypex = 'page' end if geotypex ~= nil and not (featureCollection[geotypex] or externalData[geotypex]) then printError('geotype-invalid', geotypex) break end if geotypex == 'none' then -- skip this object i = i + 1 else local mystack if externalData[geotypex or ''] then mystack = myexternal j = #mystack + 1 mystack[j] = {} mystack[j]['type'] = "ExternalData" mystack[j]['service'] = geotypex if geotypex == "page" then local page_name = args['commons'..i] if mw.ustring.find(page_name, "Data:", 1, true) == 1 then page_name = string.sub(page_name, 6) end if mw.ustring.find(page_name, ".map", -4, true) == nil then page_name = page_name .. '.map' end mystack[j]['title'] = page_name else mystack[j]['ids'] = args['ids'..i] or args['ids'] or wdid if mystack[j]['ids'] == nil then printError('ids-not-found'); break end end local mycoordinates = args['coordinates'..i] if mycoordinates == nil and (tagArgs.latitude == nil or tagArgs.longitude == nil or tagArgs.zoom == nil) then mycoordinates = getCoordinatesById(mystack[j]['ids']) end if mycoordinates ~= nil then local mypoints = getBoundsById(mystack[j]['ids'], mycoordinates) if #mypoints == 0 then mypoints = parseGeoSequence(mycoordinates, mycoordinates:find(';') and 'MultiPoint' or 'Point') end allpoints = mergePoints(allpoints, mypoints) end else args['coordinates'..i] = args['coordinates'..i] or getCoordinatesById(args['ids'..i]) if geotypex == 'circle' then if not args['radius'..i] and args['radiuskm'..i] then args['radius'..i] = args['radiuskm'..i] * 1000 end args['coordinates'..i] = circleToPolygon(args['coordinates'..i], args['radius'..i], args['edges'..i], args['turn'..i]) geotypex = 'Polygon' end mystack = myfeatures j = #mystack + 1 mystack[j] = {} mystack[j]['type'] = "Feature" mystack[j]['geometry'] = {} mystack[j]['geometry']['type'] = geotypex or findGeotype(args['coordinates'..i]) mystack[j]['geometry']['coordinates'] = parseGeoSequence(args['coordinates'..i], mystack[j]['geometry']['type']) allpoints = mergePoints(allpoints, mystack[j]['geometry']['coordinates']) addCategories(mystack[j]['geometry']['type'], i) end mystack[j]['properties'] = {} mystack[j]['properties']['title'] = args['title'..i] or (geotypex and geotypex .. i) or mystack[j]['geometry']['type'] .. i if args['image'..i] then args['description'..i] = (args['description'..i] or '') .. '[[File:' .. args['image'..i] .. '|300px]]' end mystack[j]['properties']['description'] = args['description'..i] mystack[j]['properties']['marker-size'] = args['marker-size'..i] or args['marker-size'] mystack[j]['properties']['marker-symbol'] = args['marker-symbol'..i] or args['marker-symbol'] mystack[j]['properties']['marker-color'] = args['marker-color'..i] or args['marker-color'] mystack[j]['properties']['stroke'] = args['stroke'..i] or args['stroke'] mystack[j]['properties']['stroke-opacity'] = tonumber(args['stroke-opacity'..i] or args['stroke-opacity']) mystack[j]['properties']['stroke-width'] = tonumber(args['stroke-width'..i] or args['stroke-width']) mystack[j]['properties']['fill'] = args['fill'..i] or args['fill'] mystack[j]['properties']['fill-opacity'] = tonumber(args['fill-opacity'..i] or args['fill-opacity']) i = i + 1 end end -- calculate defaults for static mapframe; maplink is dynamic if (tagArgs.latitude == nil or tagArgs.longitude == nil) and #allpoints > 0 then if tagname == "mapframe" or tagArgs.text == nil then -- coordinates needed for text in maplink tagArgs.longitude, tagArgs.latitude = getCoordCenter(allpoints) end end if tagArgs.zoom == nil then if tagname == "mapframe" then local uniquepoints = setUniquePoints(allpoints) if #uniquepoints == 1 then local coordInput = uniquepoints[1][2] .. ',' .. uniquepoints[1][1] local mybounds = getBoundsById(wdid, coordInput) -- try to fetch by area uniquepoints = mergePoints(uniquepoints, mybounds) end if #uniquepoints <= 1 then tagArgs.zoom = defaultzoom or 9 else tagArgs.zoom = getZoom(uniquepoints, tagArgs.height, tagArgs.width) end else tagArgs.zoom = defaultzoom end end local geojson = myexternal if #myfeatures > 0 then geojson[#geojson + 1] = {type = "FeatureCollection", features = myfeatures} end if args.debug ~= nil then local html = mw.text.tag{name = tagname, attrs = tagArgs, content = mw.text.jsonEncode(geojson, mw.text.JSON_PRETTY)} return 'syntaxhighlight', tostring(html) .. ' Arguments:' .. mw.text.jsonEncode(args, mw.text.JSON_PRETTY), {lang = 'json'} end if geojson and #geojson == 0 then errormessage = erromessage or '' -- previous message or void for no map data end return tagname, geojson and mw.text.jsonEncode(geojson) or '', tagArgs end function p.tag(frame) --if mw.title.new(frame:getParent():getTitle()).isContentPage and not mw.title.new(frame:getTitle()).isSubpage then -- invoked from a content page and not invoking a module subpage -- printError('not-from-content-page') --end local getArgs = require('Module:Arguments').getArgs local args = getArgs(frame) local tag, geojson, tagArgs = p._tag(args) local categories = '' if errormessage then if errormessage == '' then -- no map data return else categories = mw.message.new('Kartographer-broken-category'):inLanguage(mw.language.getContentLanguage().code):plain() return errormessage .. '[[Category:' .. categories .. ']]' end end for k, v in pairs(cat) do if v then categories = categories .. '[[Category:' .. i18n[k] .. ']]' end end return frame:extensionTag(tag, geojson, tagArgs) .. categories end return p