Modul:GeoData
Erscheinungsbild
| Dieses Modul ist getestet und für den projektweiten Gebrauch geeignet. Es kann in Vorlagen benutzt und auf Hilfeseiten erläutert werden. Entwicklungen an dem Modul sollten auf GeoData/Test und die Anwendung auf der Spielwiese getestet werden, da wiederholte Trial-and-Error-Edits die Resourcen stark belasten können. |
Dieses Modul benutzt die Wikidata-Eigenschaften:
|
Dieses Modul enthält Funktionen zur Anzeige von geografischen Koordinaten für die Vorlagen {{Coord}} und {{GeoData}}.
Die Modulbeschreibung befindet sich in Wikivoyage:GeoData.
Versionsbezeichnung auf Wikidata: 2025-11-09
Stilvorlagen
Zum Modul GeoData gehören zwei Stilvorlagen:
- für die Vorlage {{GeoData}}: Module:GeoData/styles.css,
- für die Vorlage {{Coord}}: Modul:GeoData/coordStyles.css.
Benötigte weitere Module
Hinweise
- Die obige Dokumentation wurde aus der Seite Modul:GeoData/Doku eingefügt. (bearbeiten | Versionsgeschichte) Die Kategorien für dieses Modul sollten in der Dokumentation eingetragen werden. Die Interwiki-Links sollten auf Wikidata eingepflegt werden.
- Liste der Unterseiten
-- Functions for the presentation of locations coordinate pairs
-- module variable and administration
local gd = {
moduleInterface = {
suite = 'GeoData',
serial = '2025-11-09',
item = 94472936
}
}
-- module import
-- require( 'strict' )
local cd = require( 'Module:Coordinates' )
local cm = require( 'Module:CountryData' )
local gc = require( 'Module:Great circle distance' )
local gi = require( 'Module:GeoData/i18n' )
local gp = require( 'Module:GeoData/Params' )
local lp = require( 'Module:LinkPhone' )
local wu = require( 'Module:Wikidata utilities' )
local possibleFormats = { 'f1', 'f2', 'f3', 'f4', 'dec' }
local lengthUnits = {
['1'] = { 'm', 1 },
Q11573 = { 'm', 1 },
Q828224 = { 'km', 1000 },
Q174789 = { 'mm', 0.001 },
Q218593 = { 'in', 0.0254 },
Q253276 = { 'mi', 1610 },
Q3710 = { 'ft', 0.3048 },
Q174728 = { 'cm', 0.01 },
Q848856 = { 'dam', 10 },
Q200323 = { 'dm', 0.1 }
}
local scalesByType = {
adm1st = 1000000,
adm2nd = 300000,
adm3rd = 100000,
airport = 30000,
city = 100000,
country = 10000000,
edu = 10000,
event = 50000,
forest = 50000,
glacier = 50000,
isle = 100000,
landmark = 10000,
mountain = 100000,
pass = 10000,
railwaystation = 10000,
river = 100000,
satellite = 10000000,
waterbody = 100000,
camera = 10000,
default = 300000
}
-- zoom level 19 -> 1:1000, 0 -> 500000000
local maxZoomLevel = 19
local scales = { 1000, 2000, 4000, 8000, 15000, 35000, 70000, 150000, 250000,
500000, 1000000, 2000000, 4000000, 10000000, 15000000, 35000000, 70000000,
150000000, 250000000, 500000000 }
-- Local helper functions
-- Helper function isSet
local function isSet( param )
return param and param ~= '';
end
local function setValue( value, default )
return isSet( value ) and value or default
end
-- Helper function roundScale
local function roundScale( scale )
if scale <= scales[ 1 ] then
return scales[ 1 ]
end
for i = 2, #scales, 1 do
if scale > scales[ i - 1 ] and scale <= scales[ i ] then
return scales[ i ]
end
end
return scales[ #scales ]
end
-- Helper function getZoomFromScale
local function getZoomFromScale( scale )
if scale <= scales[ 1 ] then
return maxZoomLevel
end
for i = 2, #scales, 1 do
if scale > scales[ i - 1 ] and scale <= scales[ i ] then
return maxZoomLevel + 2 - i -- because i starts from 2
end
end
return 0
end
-- helper function round
-- n: value to round
-- idp: number of digits after the decimal point
local function round( n, idp )
local m = 10^( idp or 0 )
return ( n >= 0 ) and ( math.floor( n * m + 0.5 ) / m ) or
( math.ceil( n * m - 0.5 ) / m )
end
local function checkFormat( f )
if type( f ) == 'table' then
return f
end
for i, fmt in ipairs( possibleFormats ) do
if fmt == f then
return f
end
end
return 'f1'
end
local function checkNumber( s )
return tonumber( s ) or ''
end
-- helper function for numbered precision
local function getPrec( p )
p = round( p, 0 )
if p <= -1 then
return -1
elseif p == 0 then
return 0
elseif p <= 2 then
return 2
elseif p <= 4 then
return 4
elseif p >= 5 then
return 5 -- maximum 5 decimals
end
end
-- helper function getPrecision
-- returns integer precision number
-- possible values: numbers, D, DM, DMS
local function getPrecision( prec )
local p = tonumber( prec )
if p then
return getPrec( p )
else
local DMS = { D = 0, DM = 2, DMS = 4 }
p = prec and prec:upper() or 'DMS'
return DMS[ p ] or ''
end
end
-- getPrecisionFromSize gives precision number for toDMS function calculated
-- from the maximum of the dimension of a geographic object dim or the measurement
-- error mErr of the coordinate (as a radius)
-- 1° equals about 1852 meters
-- 1852 meters give precision = 2 (1')
local function getPrecisionFromSize( dim, mErr )
dim = tonumber( dim ) or 4000
if mErr ~= '' then
mErr = tonumber( mErr ) or 0
dim = math.max( dim, 2 * mErr )
end
dim = math.max( dim, 1 )
-- 2 * 60 * 1852 = 222240
return getPrec( math.ceil ( math.log10 ( 222240 / dim ) ) )
end
-- getExtraParameters returns a string with extra Geohack parameters as it is
-- used by Special:Mapsources and {{#coordinates}}
local function getExtraParameters( args )
local params = { 'type', 'dim', 'globe', 'region' }
local s = 'scale:' .. args.scale
for i, param in ipairs( params ) do
if isSet( args[ param ] ) then
s = s .. '_' .. param .. ':' .. args[ param ]
end
end
return s
end
-- Helper function for microformat pattern creation
local function getMicroformat( args )
return '<span class="h-card listing-coordinates" style="display: none;">' ..
'<span class="p-geo geo">' ..
'<span class="p-latitude latitude">' .. args.lat .. '</span>, ' ..
'<span class="p-longitude longitude">' .. args.long .. '</span>' ..
'</span>' ..
'<span class="p-name">' .. args.name .. '</span>' ..
'</span>'
end
-- Helper function adding template data
local function templateStyles( frame, styles )
return frame:extensionTag( 'templatestyles', '', { src = styles } );
end
-- getLengthFromWD returns a length by property from WD
local function getLengthFromWD( id, p )
local w = wu.getValue( id, p )
if w == '' then
return 0
end
local a = tonumber( w.amount ) or 0
local u = lengthUnits[ w.unit ]
if u then
u = u[ 2 ] -- get factor
else
u = wu.getValue( w.unit, gi.properties.conversionToSI )
u = u == '' and 0 or tonumber( u.amount ) or 0
end
return a * u
end
-- Helper function to get data from Wikidata by id
-- gets name, dimension, latitude, and longitude
-- returns an error if no coordinates are available
local function getParamsFromWD( args )
local coordState = {
err = not isSet( args.lat ) or not isSet( args.long ),
faultyCoordinate = false,
distanceErr = 0,
fromWD = false
}
if not coordState.err then
local objLat = cd.toDec( args.lat, 'lat', 8 )
args.lat = objLat.dec
local objLong = cd.toDec( args.long, 'long', 8 )
args.long = objLong.dec
coordState.err = ( objLat.error + objLong.error ) > 0
coordState.faultyCoordinate = coordState.err
end
if not args.wikidata then
return coordState
end
if not isSet( args.name ) then
args.name = mw.wikibase.label( args.wikidata ) or ''
end
local length = math.abs( tonumber( args.dim ) or 0 )
if length < 1 then
length = getLengthFromWD( args.wikidata, gi.properties.length )
local width = getLengthFromWD( args.wikidata, gi.properties.width )
length = math.max( length, width )
end
args.dim = length < 1 and '' or round( length )
-- getting coordinates in any case
local c = wu.getValue( args.wikidata, gi.properties.centerCoordinates )
if c == '' then
c = wu.getValue( args.wikidata, gi.properties.coordinates )
end
if c ~= '' then
c.latitude = tonumber( c.latitude )
c.longitude = tonumber( c.longitude )
-- 1° ~ 1842 m
c.precision = ( tonumber( c.precision ) or 0 ) * 1842
if not isSet( args.prec ) and c.precision >= 1 then
args.prec = round( c.precision )
end
if coordState.err then
args.lat = c.latitude
args.long = c.longitude
coordState.err = false
coordState.faultyCoordinate = false
coordState.fromWD = true
else
-- GeoData and Wikidata positions may differ
coordState.distanceErr = gc.getGcd( args.lat, args.long,
c.latitude, c.longitude )
end
end
return coordState
end
local function formatCoordinates( args, pattern )
local s, lat, long
if args.format == 'dec' then
s, lat, long = cd.getDecGeoLink( pattern, args.lat, args.long, args.precision )
else
s, lat, long = cd.getGeoLink( pattern, args.lat, args.long, '', '', '', '',
args.precision, args.format )
end
return s
end
-- Parameter check
-- cat contains tracking categories
local function checkParams( args, cat )
args.lat = args[ 1 ] or args.lat or args.NS or '' -- NS/EW: fallback
args.long = args[ 2 ] or args.long or args.EW or ''
args.name = args.name or ''
args.display = args.display and args.display:lower() or 'inline'
args.format = checkFormat( args.format and args.format:lower() or 'f1' )
args.region = args.region and args.region:upper() or ''
args.dim = checkNumber( args.dim or '' )
args.globe = args.globe and args.globe:lower() or 'earth'
args.precision = getPrecision( args.precision or '' ) -- display precission
args.prec = checkNumber( args.prec or '' ) -- measurement error
args.radius = checkNumber( args.radius or '' )
args.scale = checkNumber( args.scale or '' )
args.zoom = checkNumber( args.zoom or '' )
args.type = args.type and args.type:lower() or ''
if not isSet( args.wikidata ) or not mw.wikibase.isValidEntityId( args.wikidata ) then
args.wikidata = nil
end
if not scalesByType[ args.type ] then
args.type = ''
end
local function fIsInline( s ) -- inline in any case
return s:find( 'inline' ) or s == 'i' or s == 'it' or s == 'ti'
end
args.isInline = fIsInline( args.display )
local function fIsInTitle( s ) -- intitle in any case
return s:find( 'title' ) or s == 't' or s == 'it' or s == 'ti'
end
args.isInTitle = fIsInTitle( args.display )
if not args.isInline and not args.isInTitle then
args.isInline = true
end
if args.isInTitle and not args.wikidata then
args.wikidata = mw.wikibase.getEntityIdForCurrentPage()
end
local coordState = getParamsFromWD( args )
if coordState.fromWD then
table.insert( cat, gi.categories.fromWikidata )
end
if coordState.err then
local m = coordState.faultyCoordinate and gi.errorMsg.faultyCoordinate or ''
table.insert( cat, m .. ( args.isInTitle and gi.categories.geoWithoutCoords or
gi.categories.coordWithoutCoords ) )
return true, nil -- is fatal error
end
if coordState.distanceErr > 50 then
table.insert( cat, gi.categories.differentPositions50 )
elseif coordState.distanceErr > 25 then
table.insert( cat, gi.categories.differentPositions25 )
elseif coordState.distanceErr > 10 then
table.insert( cat, gi.categories.differentPositions )
end
country = cm.getCountryData( args.wikidata, nil )
if country.fromWD then
table.insert( cat, gi.categories.countryFromWD )
end
if not isSet( args.region ) then
args.region = country.iso_3166
end
if args.name == '' then
args.name = gi.errorMsg.missingName
table.insert( cat, gi.categories.coordWithoutName )
end
if args.scale == '' and args.type ~= '' then
args.scale = scalesByType[ args.type ]
end
if args.zoom ~= '' then
args.zoom = round( args.zoom )
if args.zoom < 0 or args.zoom > maxZoomLevel then
args.zoom = ''
end
end
if args.dim == '' and args.radius == '' and args.zoom == ''
and args.scale == '' then
args.dim = 4000
table.insert( cat, gi.categories.withoutScale )
end
if args.scale == '' and args.zoom ~= '' then
args.scale = scales[ maxZoomLevel + 1 - args.zoom ]
end
if args.dim == '' and args.radius ~= '' then
args.dim = round( 2 * args.radius )
end
if args.dim == '' and args.scale ~= '' then
args.dim = args.scale / 5
end
if args.scale == '' then
args.scale = 5 * args.dim
end
args.scale = roundScale( args.scale )
if args.zoom == '' then
args.zoom = getZoomFromScale( args.scale )
end
if args.precision == '' then -- mainly for geo/geoData
args.precision = getPrecisionFromSize( args.dim, args.prec )
end
return false, country -- no fatal error
end
-- Adding indicator tag attributes
local function addDataAttr( tag, args, country )
local function data( s )
return isSet( s ) and s or nil
end
tag:addClass( 'voy-coord-indicator' )
:attr( 'data-lat', args.lat )
:attr( 'data-lon', args.long )
:attr( 'data-zoom', args.zoom )
-- adding country- and region-related data
if country.id ~= '' then
tag:attr( 'data-country', data( country.iso_3166 ) )
:attr( 'data-adm1st', data( country.adm1st ) )
:attr( 'data-country-calling-code', data( country.cc ) )
:attr( 'data-trunk-prefix', data( country.trunkPrefix ) )
:attr( 'data-lang', data( country.lang ) )
:attr( 'data-currency', data( country.addCurrency ) )
:attr( 'data-dir', data( country.isRTL and 'rtl' or 'ltr' ) )
end
return tostring( tag )
end
-- Coordinates shown as article indicator
-- Helper function indicatorTable returns a table containing coordinate text
local function indicatorTable( text, args, country )
if country.id ~= '' then
if country.cc ~= '' then
country.trunkPrefix = lp.getTrunkPrefix( country.cc )
end
country.adm1st = country.adm1st or cm.getAdm1st( country.id )
text = text:gsub( '_region%%3A%a+&',
'_region%%3A' .. ( country.adm1st or country.iso_3166 ) .. '&' )
end
local c = mw.ustring.gsub( gi.titles.coordinates, '($1)', args.name)
local m = mw.ustring.gsub( gi.titles.mapsources, '($1)', args.name)
local tag = mw.html.create( 'div' )
:attr( 'id', 'voy-geo-indicator' )
:node( mw.html.create( 'div' )
:addClass( 'voy-icon' )
:attr( 'title', c )
:node( mw.html.create( 'span' )
:addClass( 'voy-map-globe-default' )
:wikitext( gi.titles.globeDefault )
)
:node( mw.html.create( 'span' )
:addClass( 'voy-map-globe-js' )
:css( 'display', 'none' )
:wikitext( gi.titles.globeJS )
)
)
:node( mw.html.create( 'div' )
:attr( 'class', 'voy-coords printNoLink plainlinks' )
:attr( 'title', m )
:wikitext( text )
)
return addDataAttr( tag, args, country )
end
-- indicator is not implemented in Minerva skin
-- but voy-coord-indicator is needed for MediaWiki:Gadget-MapTools.js
local function getMinervaIndicator( args, country )
local tag = mw.html.create( 'div' )
:attr( 'id', 'voy-geo-minerva-indicator' )
:css( 'display', 'none' )
:wikitext( 'GeoData' )
return addDataAttr( tag, args, country )
end
-- Display the coordinates
local function displayCoords( args, frame, isNs0 )
local cat = isNs0 and { gi.categories.hasGeo } or {} -- tracking categories
local isError, country = checkParams( args, cat )
if isError then
return table.concat( cat, '' )
end
local mf = getMicroformat( args )
local extra = getExtraParameters( args )
-- inline coordinates
local v = ''
local format
if args.isInline then
format = '<span class="printNoLink plainlinks">' .. mf ..
'[' .. gi.coordURL .. '$1_$2_' .. mw.uri.encode( extra, 'QUERY' ) ..
'&locname=' .. mw.uri.encode( args.name, 'QUERY' ) ..
' <span class="voy-coord-style" title="' .. gi.titles.latitude .. '">$3</span>' ..
' <span class="voy-coord-style" title="' .. gi.titles.longitude .. '">$4</span>]' ..
'</span>'
v = templateStyles( frame, gi.coordStyles ) .. formatCoordinates( args, format )
end
-- indicator/in-title coordinates
local w = ''
if args.isInTitle then
format = mf ..
'[' .. gi.coordURL .. '$1_$2_' .. mw.uri.encode( extra, 'QUERY' ) ..
'&locname=' .. mw.uri.encode( args.name, 'QUERY' ) .. ' $3<br />$4]'
local tab = formatCoordinates( args, format )
tab = indicatorTable( tab, args, country )
w = frame:extensionTag( 'indicator', templateStyles( frame, gi.geoStyles ) .. tab,
{ name = gi.indicatorName } ) .. getMinervaIndicator( args, country )
end
local parserFc = ''
local parserFcArgs = { args.lat, args.long, extra, name = args.name }
if args.isInTitle then
table.insert( parserFcArgs, 1, 'primary' )
end
if frame then -- adding {{#coordinates}}
parserFc = frame:callParserFunction{ name = '#coordinates',
args = parserFcArgs }
end
return v .. w .. parserFc .. table.concat( cat, '' ) ..
( isNs0 and wu.getCategories( gi.categories.properties ) or '' )
end
-- [[template:Coord]] template
-- format: f1 ... f4, dec
function gd.coord( frame )
local args = frame:getParent().args
local ns = mw.title.getCurrentTitle().namespace
args.display = 'inline'
args.precision = setValue( args.precision, '4' )
return displayCoords( args, frame, ns == 0 ) .. gp.checkParams( args, gp.coord )
end
-- [[template:Geo]] template
-- if id is set then data are alternatively used from Wikidata
-- includes primary {{#coordinate}}
-- calculates precission from the size of the geographical object
function gd.geo( frame )
local args = frame:getParent().args
local title = mw.title.getCurrentTitle()
local ns = title.namespace
args.display = 'title'
args.format = 'f2'
args.name = setValue( args.name, title.subpageText )
return displayCoords( args, frame, ns == 0 ) .. gp.checkParams( args, gp.geo )
end
return gd