Module:Canada NTS
Appearance
![]() | This Lua module is used on approximately 2,100 pages and changes may be widely noticed. Test changes in the module's /sandbox or /testcases subpages, or in your own module sandbox. Consider discussing changes on the talk page before implementing them. |
Usage
This module implements the grid reference scheme for Canada's National Topographic System. It contains functions that will determine the NTS map sheet ID for a given set of coordinates, as well as the name of the map sheet. The database of map sheet names can be found in Module:Canada NTS/data, and is sourced directly from Natural Resources Canada's Toporama page.
Functions:
- grid: Calculates and displays the NTS map sheet ID.
- name: Displays the name of the NTS map sheet.
These two functions can be called from the same invoke.
Optional parameters:
- lat=lat, where lat is a north latitude in decimal degrees.
- lon=lon, where lon is a west longitude in decimal degrees.
- link: Wraps the NTS map sheet ID in a link to a visualizer on Natural Resources Canada's website.
- area: Omits the 1:50,000 scale map sheet number, giving a 1:250,000 scale map area ID.
If lat and lon are omitted, the module will fetch its coordinates from the Wikidata item corresponding to the article in which its invoke has been transcluded.
Implements {{Canada NTS Map Sheet}}
require('Module:No globals'); -- alarm when global variables etc are used
local get_args = require ('Module:Arguments').getArgs; -- simplfy frame and parent frame argument fetching
--[[--------------------------< W I K I D A T A _ L A T _ L O N G _ G E T >------------------------------------
returns latitude and longitude from wikidata for current page; nil else
Nanaimo BC is Q16461 → latitude: 49.164166666667; longitude: -123.93638888889
TODO: accept a <qid> argument? if called with <qid> fetch lat/lon for that qid and not for current page
]]
local function wikidata_lat_lon_get()
local qid = mw.wikibase.getEntityIdForCurrentPage(); -- get the qid for the current page
if qid then
local value_t = mw.wikibase.getBestStatements (qid, 'P625')[1];
if value_t then
value_t = value_t.mainsnak.datavalue.value; --point to the value table
return value_t.latitude, value_t.longitude; -- return coordinates from value_t
end
end
end
--[[--------------------------< A R T I C L E _ N A M E _ F R O M _ Q I D _ G E T >----------------------------
returns local article name for this wiki using <qid>; nil else
TODO: if no local article fallback to en.wiki
]]
local function article_name_from_qid_get (qid)
local this_wiki_code = mw.language.getContentLanguage():getCode(); -- get this wiki's language code
if 'wikidata' == mw.site.server then
this_wiki_code = mw.getCurrentFrame():preprocess('{{int:lang}}'); -- on Wikidata so use interface language setting instead
end
local wd_article = mw.wikibase.getSitelink (qid, this_wiki_code .. 'wiki'); -- fetch article title from WD; nil when no title available at this wiki
if wd_article then
wd_article = table.concat ({':', this_wiki_code, ':', wd_article}); -- interwiki-style link without brackets if taken from WD; leading colon required
end
return wd_article; -- article title from WD; nil else
end
--[[--------------------------< _ N A M E >--------------------------------------------------------------------
This function takes in a National Tiling System ID and outputs the name of the National Topographic System map
sheet with that ID. If no map sheet has been published under that ID, the output will be blank.
]]
local function _name (args_t)
local title = mw.loadData ('Module:Canada NTS/data')
if not title[args_t[1]] then
return "";
elseif '(untitled)' == title[args_t[1]]:lower() then
return title[args_t[1]];
end
local this_article_title = mw.title.getCurrentTitle().text; -- get title of this article
local title_parts = mw.text.split (title[args_t[1]], '|'); -- title_parts[1] is link part of a wikilink; title_parts[2] is wikilink label (map name) or nil
if this_article_title == title_parts[1] then -- don't wikilink to the current article
return title_parts[2] or title_parts[1]; -- return plaintext wikilink label or plain text article title (wikilink)
end
return string.format ('[[%s]]', title[args_t[1]]); -- return wikilinked map title
end
--[[--------------------------< N A M E >----------------------------------------------------------------------
external invoke interface
This function takes in a National Tiling System ID and outputs the name of the National Topographic System map sheet with that ID.
If no map sheet has been published under that ID, the output will be blank.
{{#invoke:Canada NTS|name|<NTS map sheet ID>}}
]]
local function name (frame)
local args_t = get_args (frame);
if not args_t[1] then
return '<span style="color:#d33">National Tiling System ID required</span>'; -- TODO: better, more informative error handling
end
return _name (args_t);
end
--[[--------------------------< C O O R D _ L I M I T S _ T >--------------------------------------------------
a sequence of sequences. Each sequence in <coord_limits_t> has two coordinate sequences that defines a rectangle
identified by the northwest and southeast corners of a subsection in the NTS series map. The first coordinate sequence identifies the northwest
corner of the subsection; the second sequence identifies the southwest corner of the subsection.
]]
local coord_limits_t = {
{{44, 88}, {40, 56}}, -- series: 40, 30, 20, 10
{{48, 88}, {44, 48}}, -- series: 41, 31, 21, 11, 1
{{56, 136}, {48, 48}}, -- series: 102, 92, ..., 2; 103, 93, ..., 3
{{68, 144}, {56, 56}}, -- series: 114, 104, ..., 14; 115, 105, ..., 15; 116, 106, ..., 16
{{72, 144}, {68, 64}}, -- series: 117, 107, ..., 27
{{76, 128}, {72, 72}}, -- series: 98, 88, ..., 38
{{80, 128}, {76, 64}}, -- series: 99, 89, ..., 29
{{84, 104}, {80, 56}}, -- series: 560, 340, 120
}
--[[--------------------------< L A T _ L O N _ V A L I D A T E >----------------------------------------------
validates <lat> and <lon> to be inside one of the rectangles defined in <coord_limits_t>
returns true when in bounds; false else
]]
local function lat_lon_validate (lat, lon)
for _, coord_limit_t in ipairs (coord_limits_t) do -- loop through the rectangle sequences in <coord_limits_t>
local lat_N = coord_limit_t[1][1]; -- for readability ...
local lat_S = coord_limit_t[2][1];
local lon_W = coord_limit_t[1][2];
local lon_E = coord_limit_t[2][2];
if (lat >= lat_S) and (lat < lat_N) and (lon >= lon_E) and (lon < lon_W) then
return true; -- <lat> and <lon> locate a point within bounds so done
end
end
return false; -- <lat> and <lon> locate a point out of bounds so done
end
--[[--------------------------< E X T E N T S >----------------------------------------------------------------
Calculates bounding boxes of 1:50,000 and 1:250,000 scale map sheets/areas from latitude and longitude
]]
local function extents (lat, lon)
local belt = math.floor((lat - 40) * 4);
local strip = math.floor((lon - 48) * 2);
local s, n, e, w -- 1:50,000 scale bounding box
local a_s, a_n, a_e, a_w -- 1:250,000 scale bounding box
local lat_limits = -- Latitude limits of bounding box
{ s = belt / 4 + 40,
n = belt / 4 + 40.25,
a_s = math.floor(lat),
a_n = math.floor(lat + 1)
};
local lon_limits = {
e, w, a_e, a_w
}
-- Calculation of longitude limits is different depending on zone
if lat >= 40 and lat < 68 then -- Southern zone
lon_limits["e"] = strip / 2 + 48;
lon_limits["w"] = strip / 2 + 48.5;
lon_limits["a_e"] = math.floor(strip / 4) * 2 + 48;
lon_limits["a_w"] = math.floor(strip / 4) * 2 + 50;
elseif lat >= 68 and lat < 80 then -- Arctic zone
lon_limits["e"] = math.floor(strip / 2) + 48;
lon_limits["w"] = math.floor(strip / 2) + 49;
lon_limits["a_e"] = math.floor(strip / 8) * 4 + 48;
lon_limits["a_w"] = math.floor(strip / 8) * 4 + 52;
elseif lat >= 80 and lat < 88 then -- High Arctic zone
lon_limits["e"] = math.floor(strip / 2) + 48;
lon_limits["w"] = math.floor(strip / 2) + 49;
lon_limits["a_e"] = math.floor(strip / 8) * 4 + 48;
lon_limits["a_w"] = math.floor(strip / 8) * 4 + 52;
end
return {
south = lat_limits["s"], north = lat_limits["n"], east = lon_limits["e"], west = lon_limits["w"],
area_south = lat_limits["a_s"], area_north = lat_limits["a_n"], area_east = lon_limits["a_e"], area_west = lon_limits["a_w"]
}
end
--[[--------------------------< N T S _ I D _ F R O M _ L A T _ L O N _ G E T >--------------------------------
]]
local function nts_id_from_lat_long_get (lat, lon)
if not (lat and lon) then -- nil when missing, empty, or not a number
lat, lon = wikidata_lat_lon_get(); -- <lat>/<lon> template param(s) invalid or missing; attempt to get <lat>/<lon> from wikidata
end
-- wikidata uses negative numbers for west (and south) so must account for that
if lat then -- normalize coords; assumes that given coords are intended to be on canada nts map
if 0 > lat then
lat = lat * -1.0; -- required because wikidata coords are signed
end
if 0 > lon then
lon = lon * -1.0; -- required because wikidata coords are signed
end
else
return nil;
-- return '<span style="color:#d33">lat/long input fail</span>'; -- TODO: better, more informative error handling
end
if not lat_lon_validate (lat, lon) then
return nil;
-- return '<span style="color:#d33">lat/long input fail</span>'; -- here when one of <lat> or <lon> outside of acceptable limits
end
local series, numarea, area, sheet_inter, sheet
if lat >= 40 and lat < 68 then -- Southern zone
series = (math.floor((lon - 48) / 8) * 10) + math.floor((lat - 40) / 4) -- Calculate 1:1,000,000 map series ID
numarea = tonumber(math.floor(((lat - 40) / 4) % 1 * 4) * 10 + math.floor(((lon - 48) / 8) % 1 * 4)) -- Calculate 1:250,000 map area ID
local southern_zone_t = {[0]='A', [1]='B', [2]='C', [3]='D', [13]='E', [12]='F', [11]='G', [10]='H', [20]='I', [21]='J', [22]='K', [23]='L', [33]='M', [32]='N', [31]='O', [30]='P'};
area = southern_zone_t[numarea]; -- translate
sheet_inter = math.floor((lat % 1) * 4) * 10 + math.floor((((lon - 48) / 8) % 1 * 4) % 1 * 4) -- Calculate 1:50,000 map sheet ID
elseif lat >= 68 and lat < 80 then -- Arctic zone
series = (math.floor((lon - 48) / 8) * 10) + math.floor((lat - 40) / 4) -- Calculate 1:1,000,000 map series ID
numarea = (math.floor(lat % 4) * 10) + math.floor((lon / 4) % 2) -- Calculate 1:250,000 map area ID
local arctic_zone_t = {[0]='A', [1]='B', [11]='C', [10]='D', [20]='E', [21]='F', [31]='G', [30]='H'};
area = arctic_zone_t[numarea]; -- translate
sheet_inter = math.floor((lat % 1) * 4) * 10 + math.floor(lon % 4) -- Calculate 1:50,000 map sheet ID
elseif lat >= 80 and lat < 88 then -- High Arctic zone
if lon >= 56 and lon < 72 then -- Calculate 1:1,000,000 map series ID
if lat >= 84 then series = 121 else series = 120 end -- These are correct - Go to <https://maps.canada.ca/czs/index-en.html>, select "Overlay reference layers", then "National Tiling System grid coverage"
elseif lon >= 72 and lon < 88 then -- there are no maps above 84°N so series 121, 341, and 561 do not exist in National Topographic System, but do exist in National Tiling System
if lat >= 84 then series = 341 else series = 340 end
elseif lon >= 88 and lon < 104 then
if lat >= 84 then series = 561 else series = 560 end
elseif lon >= 104 and lon < 120 then -- These are correct - Go to <https://maps.canada.ca/czs/index-en.html>, select "Overlay reference layers", then "National Tiling System grid coverage"
if lat >= 84 then series = 781 else series = 780 end -- 780, 781, 910, or 911
elseif lon >= 120 and lon < 136 then
if lat >= 84 then series = 911 else series = 910 end
end -- Remember the difference between the National Topographic System and the National Tiling System
numarea = (math.floor(lat % 4) * 10) + (math.floor((lon / 8) + 1) % 2) -- Calculate 1:250,000 map area ID
local high_arctic_zone_t = {[0]='A', [1]='B', [11]='C', [10]='D', [20]='E', [21]='F', [31]='G', [30]='H'};
area = high_arctic_zone_t[numarea]; -- translate
sheet_inter = math.floor((lat % 1) * 4) * 10 + math.floor((lon % 8) / 2) -- Calculate 1:50,000 map sheet ID
end
local sheet_t = {[0]=1, [1]=2, [2]=3, [3]=4, [13]=5, [12]=6, [11]=7, [10]=8, [20]=9, [21]=10, [22]=11, [23]=12, [33]=13, [32]=14, [31]=15, [30]=16}
sheet = sheet_t[sheet_inter]
return series, area, sheet
end
--[[--------------------------< E X T E N T S _ F R O M _ G R I D >--------------------------------------------
Calculates bounding boxes of 1:50,000 and 1:250,000 scale map sheets/areas from map sheet ID
Argument #1 is 1:1,000,000 scale map series,
argument #2 is 1:250,000 scale map area,
argument #3 is 1:50,000 scale map sheet
]]
local function extents_from_grid ( series, area, sheet )
series = tonumber (series);
if not series then
return nil; -- couldn't convert series to number
end
sheet = (sheet and tonumber (sheet)) or 0; -- convert if present; else 0
local belt
local belt_area_south = {["M"]=12, ["N"]=12, ["O"]=12, ["P"]=12,
["L"]=8, ["K"]=8, ["J"]=8, ["I"]=8,
["E"]=4, ["F"]=4, ["G"]=4, ["H"]=4,
["D"]=0, ["C"]=0, ["B"]=0, ["A"]=0
}
local belt_area_north = {["G"]=12, ["H"]=12,
["F"]=8, ["E"]=8,
["C"]=4, ["D"]=4,
["B"]=0, ["A"]=0
}
local belt_sheet = {[13]=3, [14]=3, [15]=3, [16]=3,
[12]=2, [11]=2, [10]=2, [9] =2,
[5] =1, [6] =1, [7] =1, [8] =1,
[4] =0, [3] =0, [2] =0, [1] =0
}
if series >= 120 then
belt = 160 + series % 10 * 16 + (belt_area_north[area] or 0) + (belt_sheet[sheet] or 0)
elseif series < 120 and series % 10 * 16 >= 112 then
belt = series % 10 * 16 + (belt_area_north[area] or 0) + (belt_sheet[sheet] or 0)
else
belt = series % 10 * 16 + (belt_area_south[area] or 0) + (belt_sheet[sheet] or 0)
end
local strip
local strip_series_high_arctic = {[12] = 16,
[34] = 48,
[56] = 80,
[78] = 112,
[91] = 144
}
local strip_area_southern = {["M"]=12, ["N"]=8, ["O"]=4, ["P"]=0,
["L"]=12, ["K"]=8, ["J"]=4, ["I"]=0,
["E"]=12, ["F"]=8, ["G"]=4, ["H"]=0,
["D"]=12, ["C"]=8, ["B"]=4, ["A"]=0
}
local strip_area_arctic = {["G"]=8, ["H"]=0,
["F"]=8, ["E"]=0,
["C"]=8, ["D"]=0,
["B"]=8, ["A"]=0
}
local strip_area_high_arctic = {["G"]=16, ["H"]=0,
["F"]=16, ["E"]=0,
["C"]=16, ["D"]=0,
["B"]=16, ["A"]=0
}
local strip_sheet_southern = {[13]=3, [14]=2, [15]=1, [16]=0,
[12]=3, [11]=2, [10]=1, [ 9]=0,
[ 5]=3, [ 6]=2, [ 7]=1, [ 8]=0,
[ 4]=3, [ 3]=2, [ 2]=1, [ 1]=0
}
local strip_sheet_arctic = {[13]=6, [14]=4, [15]=2, [16]=0,
[12]=6, [11]=4, [10]=2, [ 9]=0,
[ 5]=6, [ 6]=4, [ 7]=2, [ 8]=0,
[ 4]=6, [ 3]=4, [ 2]=2, [ 1]=0
}
local strip_sheet_high_arctic = {[13]=12, [14]=8, [15]=4, [16]=0,
[12]=12, [11]=8, [10]=4, [ 9]=0,
[ 5]=12, [ 6]=8, [ 7]=4, [ 8]=0,
[ 4]=12, [ 3]=8, [ 2]=4, [ 1]=0
}
local east_limit, west_limit; -- For 1:50,000 scale map sheet
local area_east_limit, area_west_limit; -- For 1:250,000 scale map area
if series >= 120 then
strip = strip_series_high_arctic[math.floor(series / 10)]
+ (strip_area_high_arctic[area] or 0)
+ (strip_sheet_high_arctic[sheet] or 0);
east_limit = strip * 0.5 + 48
west_limit = (strip + 4) * 0.5 + 48
area_east_limit = math.floor(strip / 16) * 8 + 48
area_west_limit = math.floor((strip + 16) / 16) * 8 + 48
elseif series < 120 and math.floor(series % 10) >= 7 then
strip = math.floor(series / 10) * 16
+ (strip_area_arctic[area] or 0)
+ (strip_sheet_arctic[sheet] or 0);
east_limit = strip * 0.5 + 48
west_limit = (strip + 2) * 0.5 + 48
area_east_limit = math.floor(strip / 8) * 4 + 48
area_west_limit = math.floor((strip + 8) / 8) * 4 + 48
else
strip = math.floor(series / 10) * 16
+ (strip_area_southern[area] or 0)
+ (strip_sheet_southern[sheet] or 0);
east_limit = strip * 0.5 + 48
west_limit = (strip + 1) * 0.5 + 48
area_east_limit = math.floor(strip / 4) * 2 + 48
area_west_limit = math.floor((strip + 4) / 4) * 2 + 48
end
local grid_limits = {
["south"] = belt * 0.25 + 40,
["north"] = belt * 0.25 + 40.25,
["area_south"] = math.floor(belt / 4) + 40,
["area_north"] = math.floor(belt / 4) + 41,
["east"] = east_limit,
["west"] = west_limit,
["area_east"] = area_east_limit,
["area_west"] = area_west_limit
}
return grid_limits
end
--[[--------------------------< G R I D >----------------------------------------------------------------------
This function takes a Canadian National Tiling System map sheet ID, or latitude and longitude either as input or
from the wikidata qid of the current page and creates a url for the map sheet. Latitude and longitude are in
decimal degrees, and automatically assumed to be north and west, as no Canadian territory lies in the southern
or eastern hemispheres.
{{#invoke:Canada NTS|grid}} -- For 1:50,000 scale map sheet ID, using coordinates from Wikidata entry corresponding to current article
{{#invoke:Canada NTS|grid|<series>|<area>|<sheet>}} -- For 1:50,000 scale map sheet ID, using NTS ID (see Template:Canada NTS Map Sheet)
{{#invoke:Canada NTS|grid|lat=<number>|lon=<number>}} -- For 1:50,000 scale map sheet ID, using coordinates specified in argument
optional parameters:
|area= to obtain a 1:250,000 scale map area ID instead
|link= to create an appropriate link to the Canadian government's Geospatial Data Extraction tool
|name= to append the map name (from Module:Canada_NTS/data) to the output
]]
local function grid (frame)
local args_t = get_args (frame); -- fetch frame and parent frame parameters into a single table
local lat, lon = tonumber(args_t.lat), tonumber(args_t.lon);
-- flags to control what the output looks like
local print_area = 'yes' == args_t.area; -- when true, render only the 'area' portion '30M' instead of sheet '30M11'
local print_link = 'yes' == args_t.link; -- when true, format the area or sheet output as an external link
local print_name = 'yes' == args_t.name; -- when true, append map name from ~/data
local series, area, sheet;
for k, v in pairs (args_t) do
if 1 == k and v:match ('^%d+$') then -- args_t[1] to series; must be a number
series = v;
elseif 2 == k and v:match ('^[A-P]$') then -- args_t[2] to area; must one uppercase letter A-P
area = v;
elseif 3 == k and v:match ('^%d+$') then -- args_t[3] to sheet; must be a number
sheet = v;
end
end
if series and not sheet then -- if we have nts id without sheet (an area-only)
print_area = true; -- set this so that later we create the correct output
end
if not series or not area then
series, area, sheet = nts_id_from_lat_long_get (lat, lon);
if not series then
return '<span style="color:#d33">lat/long input fail</span>'; -- TODO: better, more informative error handling
end
end
local output = print_area and (series .. area) or (series .. area .. sheet);
print_name = print_name and (' ' .. _name ({output})) or ''; -- reuse <print_name> to hold name or empty string
local extents_t = {}; -- to hold bounding box coordinates for url
if print_link and lat then -- when we have lat/lon
extents_t = extents (lat, lon); -- get a table of sheet and area extents from lat/lon
elseif print_link then -- when we have nts id
extents_t = extents_from_grid (series, area, sheet); -- get a table of sheet and area extents from nts id
else
print_link = nil; -- unset because we can't create a link
end
if print_link then
local ext_link_fmt_str = '[https://maps.canada.ca/czs/index-en.html?bbox=-%s,%s,-%s,%s&name=NTS_map_sheet_%s %s]';
output = print_area and string.format (ext_link_fmt_str, extents_t.area_west, extents_t.area_south, extents_t.area_east, extents_t.area_north, output, output) or
string.format (ext_link_fmt_str, extents_t.west, extents_t.south, extents_t.east, extents_t.north, output, output);
end
output = output .. print_name; -- append name or empty string
return output; -- and done
end
--[[--------------------------< E X P O R T E D F U N C T I O N S >------------------------------------------
]]
return {
grid = grid,
name = name,
}