end  
---
Check if the name of a template matches the known coord templates or wrappers
(in L10n.template.coord). The name is normalised when checked, so e.g. the names
"Coord", "coord", and " Coord" all return true.
@param {string} name
@returns {boolean} true if it is a coord template or wrapper, false otherwise
---
function util.isCoordTemplateOrWrapper(name)
name = mw.text.trim(name)
local inputTitle = mw.title.new(name, 'Template')
if not inputTitle then
return false
end  
-- Create (or reuse) mw.title objects for each known coord template/wrapper.
-- Stored in L10n.template.title so that they don't need to be recreated
-- each time this function is called
if not L10n.template.titles then
L10n.template.titles = {}
for _, v in pairs(L10n.template.coord) do
table.insert(L10n.template.titles, mw.title.new(v, 'Template'))
end
end  
for _, templateTitle in pairs(L10n.template.titles) do
if mw.title.equals(inputTitle, templateTitle) then
return true
end
end  
return false
end  
---
Recursively extract coord templates which have a name parameter.
@param {string} wikitext
@returns {table} table sequence of coord templates
---
function util.extractCoordTemplates(wikitext)
local output = {}
local templates = mw.ustring.gmatch(wikitext, '{%b{}}')
local subtemplates = {}
for template in templates do
local templateName = mw.ustring.match(template, '
--nil} page Page name, or name#section, to get named coordinates
from. If the name is omitted, i.e. #section or nil or empty string, then
the current page will be used.
@returns {table} sequence of {coord, name, description} tables where coord is
the coordinates in a format suitable for #util.parseCoords, name is a string,
and description is a string (coordinates in a format suitable for displaying
to the reader). If for some reason the name can't be found, the description
is nil and the name contains display-format coordinates.
@throws {L10n.error.noNamedCoords} if no named coordinates are found.
---
function util.getNamedCoords(page)
if transcluder == nil then
-- load Module:Transcluder lazily so it is only transcluded on pages that
-- actually use named coordinates
transcluder = require("Module:Transcluder")
end
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 = transcluder.get(section and name.."#"..section or name)
local coordTemplates = util.extractCoordTemplates(pageWikitext)
if #coordTemplates == 0 then error(string.format(L10n.error.noNamedCoords, page or name), 0) end
local = mw.getCurrentFrame
local sep = "________"
local expandedContent = :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%-dec\".->(.-)</span>")
if coord then
local name = (
-- name specified by a wrapper template, e.g Name
mw.ustring.match(expandedTemplate, "<span class=\"mapframe%-coord%-name\">(.-)</span>") or
-- name passed into coord template
mw.ustring.match(expandedTemplate, "<span class=\"fn org\">(.-)</span>") or
-- default to the coordinates if the name can't be retrieved
coord
local description = name ~= coord and coord
local coord = mw.ustring.gsub(coord, "[° ]", "_")
table.insert(namedCoords, {coord=coord, name=name, description=description})
end
end
if #namedCoords == 0 then error(string.format(L10n.error.noNamedCoords, page or name), 0) end
return namedCoords
end  
---
Parse coordinate values from the params passed in a GeoHack url (such as
//tools.wmflabs.org/geohack/geohack.php?pagename=Example&params=1_2_N_3_4_W_ or
//tools.wmflabs.org/geohack/geohack.php?pagename=Example&params=1.23_S_4.56_E_ )
or non-url string in the same format (such as `1_2_N_3_4_W_` or `1.23_S_4.56_E_`)
@param {string} coords string containing coordinates
@returns {number, number} latitude, longitude
---
function util.parseCoords(coords)
local coordsPatt
if mw.ustring.find(coords, "params=", 1, true) then
-- prevent false matches from page name, e.g. ?pagename=Lorem_S._Ipsum
coordsPatt = 'params=([_%.%d]+[NS][_%.%d]+[EW])'
else
-- not actually a geohack url, just the same format
coordsPatt = '[_%.%d]+[NS][_%.%d]+[EW]'
end
local parts = mw.text.split((mw.ustring.match(coords, coordsPatt) 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  
---
Get coordinates from a Wikidata item
@param {string} item_id Wikidata item id (Q number)
@returns {number, number} latitude, longitude
@throws {L10n.error.noCoords} if item_id is invalid or the item does not exist
@throws {L10n.error.wikidataCoords} if the the item does not have a P625
statement (coordinates), or it is set to "no value"
---
function util.wikidataCoords(item_id)
if not (item_id and mw.wikibase.isValidEntityId(item_id) and 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' or coordStatements[1].mainsnak.snaktype == 'somevalue') )
if hasNoValue then
error(L10n.error.wikidataCoords, 0)
end
local wdCoords = coordStatements[1]['mainsnak']['datavalue']['value']
return tonumber(wdCoords['latitude']), tonumber(wdCoords['longitude'])
end  
---
Creates a polygon that approximates a circle
@param {number} lat Latitude
@param {number} long Longitude
@param {number} radius Radius in metres
@param {number} n Number of edges for the polygon
@returns {table} sequence of {latitude, longitude} table sequences, where
latitude and longitude are both numbers
---
function util.circleToPolygon(lat, long, radius, n) -- n is number of edges
-- 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(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  
---
Get the number of key-value pairs in a table, which might not be a sequence.
@param {table} t
@returns {number} count of key-value pairs
---
function util.tableCount(t)
local count = 0
for k, v in pairs(t) do
count = count + 1
end
return count
end  
--nil} count of key-value pairs of subtable, or nil if subtables
have different counts
---
function util.subTablesCount(t)
local count = nil
for k, v in pairs(t) do
if count == nil then
count = util.tableCount(v)
elseif count ~= util.tableCount(v) then
return nil
end
end
return count
end  
---
Splits a list into a table sequence. The items in the list may be separated by