Open main menu
Home
Random
Donate
Recent changes
Special pages
Community portal
Preferences
About Stockhub
Disclaimers
Search
User menu
Talk
Contributions
Create account
Log in
Editing
Module:Sandbox/Wnt/FindFeatures
Warning:
You are not logged in. Your IP address will be publicly visible if you make any edits. If you
log in
or
create an account
, your edits will be attributed to your username, along with other benefits.
Anti-spam check. Do
not
fill this in!
-- 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:Sandbox/Wnt/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:Sandbox/Wnt/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
Summary:
Please note that all contributions to Stockhub may be edited, altered, or removed by other contributors. If you do not want your writing to be edited mercilessly, then do not submit it here.
You are also promising us that you wrote this yourself, or copied it from a public domain or similar free resource (see
Stockhub:Copyrights
for details).
Do not submit copyrighted work without permission!
Cancel
Editing help
(opens in new window)
Templates used on this page:
Template:Module other
(
edit
)
Template:Module rating
(
edit
)
Template:Ombox
(
edit
)
Module:Arguments
(
edit
)
Module:Message box
(
edit
)
Module:Message box/configuration
(
edit
)
Module:Message box/ombox.css
(
edit
)
Module:Sandbox/Wnt/FindFeatures/doc
(
edit
)
Module:Yesno
(
edit
)