Module:FindFeatures
لاسوند لپاره ددې موډيول کېدای سی په Module:FindFeatures/لاسوند کي وي
-- This module finds features with coordinates in a certain area on a globe.
-- It uses other modules containing database files, which can be generated by Module:FindFeatures/displayDatabase
-- These files can be edited manually, so for brevity they use simple indexes:
-- * recordname = dataitem[1]
-- * latitude = dataitem[2][1]
-- * longitude = dataitem[2][2]
local getArgs = require('Module:Arguments').getArgs
local p = {}
local DEFAULTHITS = 5
local DEFAULTSHOWDIST = 1
local GLOBES = mw.loadData('Module:FindFeatures/globes') or {}
local GLOBEDATA = {}
local i = 1
while GLOBES[i] do
local fcn = GLOBES[i][1]
GLOBEDATA[fcn] = {GLOBES[i][2], GLOBES[i][3], GLOBES[i][4], "Module:FindFeatures/"..fcn}
p[fcn] = function (frame)
return p.main(frame, unpack(GLOBEDATA[fcn]))
end
p[mw.ustring.gsub(fcn, "(.)", mw.ustring.lower, 1)] = p[fcn]
i = i + 1
end
local DEBUGLOG = ""
local WARNCATEGORIES = {}
function selfLink(link, current, distance)
-- link may contain "|" piping but should otherwise be ready to go in [[ ]]
local link = mw.ustring.gsub(link, "%s*|.*$", "") or link
if (link == current) then
if (distance and distance > 0.0001) then
table.insert(WARNCATEGORIES, "position")
end
return true
else
return nil
end
end
function warnings()
local messages = ""
local i = 1
while WARNCATEGORIES[i] do
messages = messages .. "[[Category: Errors reported by Module:FindFeatures/" .. WARNCATEGORIES[i] .. "]]"
i = i + 1
end
return messages
end
function parseBounds(args)
local i
local norths = {}
local easts = {}
for i = 1, 4 do
if args[i] then
local value, direction = parseBound(args[i])
if (direction == "S") or (direction == "W") then value = 0 - value end
if direction == "N" or direction == "S" then
table.insert(norths, value)
elseif direction == "E" or direction == "W" then
table.insert(easts, value)
end
end
end
if (#norths == 2 and #easts == 2) then
local bound = {}
if norths[1] > norths[2] then
bound.N, bound.S = norths[1], norths[2]
else
bound.N, bound.S = norths[2], norths[1]
end
-- screw the wrap. I don't even care anymore. Let the user think about it.
if easts[1] > easts[2] then
bound.E, bound.W = easts[1], easts[2]
else
bound.E, bound.W = easts[2], easts[1]
end
return bound
end
end
function tidyNum(text)
text = mw.ustring.gsub(text, " ", "")
text = mw.ustring.gsub(text, ",", ".")
return tonumber(text)
end
function parseValue(text)
-- extract 3 or 2 or 1 values from the string. Can contain . or , as a decimal, no spaces allowed.
local d, m, s = mw.ustring.match(text,"(%-?%d+[%.,]?%d*)[%a%c%s%z!@#$%%^&%*%(%)%=|\~']+(%-?%d+[%.,]?%d*)[%a%c%s%z!@#$%%^&%*%(%)%=|\~']+(%-?%d+[%.,]?%d*)")
if not d then d, m = mw.ustring.match(text,"(%-?%d+[%.,]?%d*)[%a%c%s%z!@#$%%^&%*%(%)%=|\~']+(%-?%d+[%.,]?%d*)") end
if not d then d = mw.ustring.match(text,"(%-?%d+[%.,]?%d*)") end
if d then
d = tidyNum(d or "0") + tidyNum(m or "0")/60 + tidyNum(s or "0")/3600
end
return d
end
function parseDirection(text)
local direction = mw.ustring.match(text,"%A([NSEWnsew])%A") or mw.ustring.match(text,"^([NSEWnsew])%A") or mw.ustring.match(text,"%A([NSEWnsew])$")
if (not direction) then
direction = mw.ustring.match(text,"([Nn])[Oo][Rr][Tt][Hh]") or mw.ustring.match(text,"([Ss])[Oo][Uu][Tt][Hh]") or mw.ustring.match(text,"([Ee])[Aa][Ss][Tt]") or mw.ustring.match(text,"([Ww])[Ee][Ss][Tt]")
end
if direction then direction = mw.ustring.upper(direction) end
return direction
end
function parseBound(text)
-- note: currently does NOT hunt for deg, min, sec variations. ASSuMEs that order.
-- analogous to parseCoord, but we just want one number and direction. But direction is mandatory.
-- What to do when presented with "47 40 N": assume degree and minute
-- "47,40 N": assume European decimal
-- "47, 40 N" : assume degree and minute, I guess
-- "47. 40 N" : assume US-style decimal, I guess
-- this logic may be contested, esp. as it gives different results for different decimal types.
-- therefore, for both "guess" issues and even 47,40 N, the alternate way is: if there are ONLY the two
-- numbers separated by space both are considered one, but if there are more, consider them two.
local value = parseValue(text)
-- single letter, can be NSEWnsew, could be beginning or end
local direction = parseDirection(text)
return value, direction
end
function parseCoord(text)
local text = mw.ustring.upper(text) -- we're only getting direction letters and numbers here
local coord = {}
-- maybe it's a Coord call like "{{Coord|37.3|N|259.0|E|globe:Mars_type:mountain}}" - then only search the template
text = mw.ustring.match(text,"{{COORD(.-)}}") or text
-- maybe it's a simple coordinate like 37N,33E?
-- note: currently does NOT hunt for deg, min, sec variations. ASSuMEs that order.
-- In this case, parsing what to do based on three numbers starts to fall apart (what if there are five?)
-- Instead, look for the direction markers first, then split into two bound parsing problems
local first, second = mw.ustring.match(text,"^(.-%A)[NSEW](%A.-)$")
if first and second and mw.ustring.match(first,"%d") then
coord[1] = parseValue(first)
second = mw.ustring.match(second, "^(.-%A)[NSEW]%A.-$") or mw.ustring.match(second, "^(.-%A)[NSEW]$") or second
coord[2] = parseValue(second)
if not (coord[1] and coord[2]) then return nil end
else
-- last ditch effort: take the first two numbers in the section, WHATEVER they are. Can be signed.
coord[1], coord[2] = mw.ustring.match(text,"(%-?%d+[%.,]?%d*)[%a%c%s%z!@#$%%^&%*%(%)%=]+(%-?%d+[%.,]?%d*)")
if not (coord[1] and coord[2]) then return nil end
coord[1] = tidyNum(coord[1])
coord[2] = tidyNum(coord[2])
end
-- at this point the amounts of coord[1] (lat) and coord[2] (lon) are set, but what directions?
local firstdir = parseDirection(text)
local seconddir = firstdir
if firstdir then
frag = text
repeat -- I just keep the first letter of the direction, not the context, so need to run forward to it
frag = mw.ustring.match(frag, firstdir .. "(.*)$")
seconddir = parseDirection(frag)
until seconddir ~= firstdir
end
-- invert signs for west, south positions
if (firstdir == "W" or firstdir == "S") then
coord[1] = 0 - coord[1]
end
if (seconddir == "W" or seconddir == "S") then
coord[2] = 0 - coord[2]
end
-- if first is E/W, put it second
if (firstdir == "W" or firstdir == "E") then
coord[1], coord[2] = coord[2], coord[1]
end
-- default without directions specified: first = latitude, no sign reversal
if (not firstdir) then
firstdir = "N"
end
if (not seconddir) then
seconddir = "E"
end
if (seconddir == "N" or seconddir == "S" or firstdir == "E" or firstdir == "W") then
table.insert(WARNCATEGORIES, "coordinates")
return nil
end
coord[2] = (coord[2] + 180) % 360 - 180
-- at this point firstdir and seconddir no longer mean anything - direction is in the + or - and first or second position
return coord
end
function display(dataitem, globe, distance)
local recordname, coord1, coord2 = dataitem[1], dataitem[2][1], dataitem[2][2]
local dir1, dir2
-- distance comes as a prerounded number of km, leaves as a string
distance = (distance ~= nil) and (": " .. tostring(distance) .. " km") or ""
-- The Coord template is absolutely up on its hind legs demanding this for non-Earth globes - see
-- https://en.wikipedia.org/wiki/Template_talk:Infobox_mill_building. Needs fixing.
if coord1<0 then
dir1 = "S"
coord1 = 0 - coord1
else
dir1 = "N"
end
if coord2<0 then
dir2 = "W"
coord2 = 0 - coord2
else dir2 = "E"
end
return '[['..recordname..']] ({{Coord|' .. coord1 .. "|" .. dir1 .. "|" .. coord2 .. "|" .. dir2 .. "|globe:" .. globe .. "}}" .. distance .. ")"
end
function inBounds(datapoint, region)
return (datapoint[2][1] < region.N and datapoint[2][1] > region.S and datapoint[2][2] > region.W and datapoint[2][2] < region.E)
end
function haversine(radians)
return (1 - math.cos(radians))/2
end
function inverseHaversine(number)
if number > 1 then number = 1 end
if number < -1 then number = -1 end
return 2 * math.asin(number ^ 0.5)
end
function haversineFunction(lat1, lon1, lat2, lon2)
local rLat1 = lat1 * math.pi / 180
local rLat2 = lat2 * math.pi / 180
local rLon1 = lon1 * math.pi / 180
local rLon2 = lon2 * math.pi / 180
-- returns d/r; must be multiplied by planetary radius to get a distance
return inverseHaversine(haversine(rLat2 - rLat1) + math.cos(rLat1)*math.cos(rLat2)*haversine(rLon2 - rLon1))
end
function inRadius(datapoint, region)
local lat = datapoint[2][1]
local lon = datapoint[2][2]
local clat = region.center[1]
local clon = region.center[2]
local distance = haversineFunction(lat, lon, clat, clon)
return ((not region.threshold) or distance < region.threshold) and distance
end
function p._main(region, pRadius, eRadius, database, globe, suppress, current)
-- default list style; others not implemented
local outprefix = ""
local delimiter = ", "
local outsuffix = ""
local outarray = {}
local criterion
-- ndatabase = "#database"; it's a pseudo table. If there's a dumber way to do this let me know.
local ndatabase = 1
while database[ndatabase] do
ndatabase = ndatabase + 1
end
ndatabase = ndatabase - 1
if region.type == "circle" then
local localRadius = ((pRadius * math.sin(region.center[1]*math.pi/180))^2 + (eRadius * math.cos(region.center[1]*math.pi/180))^2)^0.5
if region.radius then region.threshold = region.radius / localRadius end
if region.hits then
local hits = {}
for i = 1, ndatabase do
-- presently this isn't the real distance; it's relative to radius/threshold
local distance = inRadius(database[i], region) * localRadius
-- if radius isn't defined, everything is inRadius
if distance then
-- table is ranked from 1 to hits. Insert hit at the lowest position where there
-- is either a vacancy or the distance is currently greater.
-- Table entries are 1.. hits containing {distance, database[i]}
local p = region.hits
while (p > 0) and ((hits[p] == nil) or (hits[p][1] > distance)) do
p = p - 1
end
if (p < region.hits) then
if not (suppress and selfLink(database[i][1], current, distance)) then
table.insert(hits, p + 1, {distance, database[i]})
table[region.hits + 1] = nil -- scrap most distant entry
end
end
end
end
for i = 1, region.hits do
table.insert(outarray, display(hits[i][2], globe, region.showdist and math.floor(hits[i][1]/region.showdist)*region.showdist))
end
else
criterion = inRadius
end
else
criterion = inBounds
end
if criterion then
for i = 1, ndatabase do
if (criterion(database[i], region)) and not (suppress and selfLink(database[i][1], current, distance)) then
table.insert(outarray, display(database[i], globe, nil))
end
end
end
return outprefix .. table.concat(outarray, delimiter) .. outsuffix
end
function p.main(frame, globe, pRadius, eRadius, datafile)
-- no presets - look up polar, equator, datafile from parameters
-- begin processing args here:
local args = getArgs(frame)
globe = args.globe or globe
pRadius = args.polar or pRadius
eRadius = args.equator or eRadius
datafile = args.datafile or datafile -- these values override the presets
if not (globe and pRadius and eRadius and datafile) then
table.insert(WARNCATEGORIES, "parameters")
return warnings()..DEBUGLOG
end
local region = {}
if args.center then
region.type = "circle"
region.center = parseCoord(args.center)
region.radius = args.radius
region.showdist = args.showdist and (tonumber(args.showdist) or DEFAULTSHOWDIST)
region.hits = args.hits and tidyNum(args.hits)
if (not region.hits) and (not region.radius) then region.hits = DEFAULTHITS end
else
region = parseBounds(args)
if (not region) then
table.insert(WARNCATEGORIES, "bounds")
return warnings() .. DEBUGLOG end
region.type = "square"
end
database = mw.loadData(datafile)
-- may write more generally; for now parameter 'suppress' means don't show link to the current article
if args.suppress then args.suppress = {self = true} end
current = mw.title.getCurrentTitle().fullText
if args.nowiki then
return frame:preprocess("<pre><nowiki>"..tostring(p._main(region, pRadius, eRadius, database, globe, args.suppress, current)).."</nowiki></pre>") .. warnings() .. DEBUGLOG
else
return frame:preprocess(tostring(p._main(region, pRadius, eRadius, database, globe, args.suppress, current))) .. warnings() .. DEBUGLOG
end
end
return p