Redigerer
Modul:Map
Hopp til navigering
Hopp til søk
Advarsel:
Du er ikke innlogget. IP-adressen din vil bli vist offentlig om du redigerer. Hvis du
logger inn
eller
oppretter en konto
vil redigeringene dine tilskrives brukernavnet ditt, og du vil få flere andre fordeler.
Antispamsjekk.
Ikke
fyll inn dette feltet!
-- 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: 20210211 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 -- Recursively extract coord templates which have a name parameter. -- from en:Module:Mapframe local function extractCoordTemplates(wikitext) local output = {} local templates = mw.ustring.gmatch(wikitext, '{%b{}}') local subtemplates = {} for template in templates do local name = mw.ustring.match(template, '{{([^}|]+)') -- get the template name local nameParam = mw.ustring.match(template, "|%s*name%s*=%s*[^}|]+") if not nameParam then nameParam = mw.ustring.match(template, "|%s*nom%s*=%s*[^}|]+") end if mw.ustring.lower(mw.text.trim(name)) == 'coord' then if nameParam then table.insert(output, template) end elseif mw.ustring.find(template, 'coord') then local subOutput = extractCoordTemplates(mw.ustring.sub(template, 2)) for _, t in pairs(subOutput) do table.insert(output, t) end end end -- ensure coords are not using title display for k, v in pairs(output) do output[k] = mw.ustring.gsub(v, "|%s*display%s*=[^|}]+", "|display=inline") end return output end -- Gets all named coordiates from a page or a section of a page. -- dependency: Module:Transcluder local function getNamedCoords(page) local parts = mw.text.split(page or "", "#", true) local name = parts[1] == "" and mw.title.getCurrentTitle().prefixedText or parts[1] local section = parts[2] local pageWikitext = require('Module:Transcluder').get(section and name.."#"..section or name) local coordTemplates = extractCoordTemplates(pageWikitext) local frame = mw.getCurrentFrame() local sep = "________" local expandedContent = frame:preprocess(table.concat(coordTemplates, sep)) local expandedTemplates = mw.text.split(expandedContent, sep) local namedCoords = {} for _, expandedTemplate in pairs(expandedTemplates) do local coord = mw.ustring.match(expandedTemplate, "<span class=\"geo\">(.-)</span>") if coord then coord = mw.ustring.gsub(coord, ";", ",") local name = mw.ustring.match(expandedTemplate, "&title=(.-)<span") or coord name = mw.uri.decode(name) local description = name ~= coord and coord table.insert(namedCoords, {coord=coord, name=name, description=description}) end end return namedCoords end -- Main function local function main(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', 'from'} 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, ['named'] = 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 geotypex == 'named' then local namedCoords = getNamedCoords(args['from'..i]) mystack = myfeatures for _, namedCoord in pairs(namedCoords) do j = #mystack + 1 mystack[j] = {} mystack[j]['type'] = "Feature" mystack[j]['geometry'] = {} mystack[j]['geometry']['type'] = "Point" mystack[j]['geometry']['coordinates'] = parseGeoSequence(namedCoord.coord, 'Point') allpoints = mergePoints(allpoints, mystack[j]['geometry']['coordinates']) mystack[j]['properties'] = {} mystack[j]['properties']['title'] = namedCoord.name mystack[j]['properties']['description'] = namedCoord.description 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'] end break elseif 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 local function addCat(cat) local categories = '' for k, v in pairs(cat) do if v then categories = categories .. '[[Category:' .. i18n[k] .. ']]' end end return categories end local function errorMessage(message) if message == '' then -- no map data return else categories = mw.message.new('Kartographer-broken-category'):inLanguage(mw.language.getContentLanguage().code):plain() return message .. '[[Category:' .. categories .. ']]' end end function p.tag(frame) -- entry point from invoke local getArgs = require('Module:Arguments').getArgs local args = getArgs(frame) local tag, geojson, tagArgs = main(args) if errormessage then return errorMessage(errormessage) end return frame:extensionTag(tag, geojson, tagArgs) .. addCat(cat) end function p._tag(args) -- entry point from require local tag, geojson, tagArgs = main(args) if errormessage then return errorMessage(errormessage) end return mw.getCurrentFrame():extensionTag(tag, geojson, tagArgs) .. addCat(cat) end return p
Redigeringsforklaring:
Merk at alle bidrag til Wikisida.no anses som frigitt under Creative Commons Navngivelse-DelPåSammeVilkår (se
Wikisida.no:Opphavsrett
for detaljer). Om du ikke vil at ditt materiale skal kunne redigeres og distribueres fritt må du ikke lagre det her.
Du lover oss også at du har skrevet teksten selv, eller kopiert den fra en kilde i offentlig eie eller en annen fri ressurs.
Ikke lagre opphavsrettsbeskyttet materiale uten tillatelse!
Avbryt
Redigeringshjelp
(åpnes i et nytt vindu)
Forhåndsvis en side som bruker denne malen
Mal som brukes på denne siden:
Modul:Map/dok
(
rediger
)
Navigasjonsmeny
Personlige verktøy
Ikke logget inn
Brukerdiskusjon
Bidrag
Opprett konto
Logg inn
Navnerom
Modul
Diskusjon
English
Visninger
Les
Rediger kilde
Vis historikk
Mer
Navigasjon
Forside
Siste endringer
Tilfeldig side
Hjelp til MediaWiki
Verktøy
Lenker hit
Relaterte endringer
Spesialsider
Sideinformasjon