Jump to content

Module:Canada NTS

Permanently protected module
From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by Trappist the monk (talk | contribs) at 15:46, 8 April 2022 (change to named parameters;). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.

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].mainsnak.datavalue.value;	--point to the value table
		return value_t.latitude, value_t.longitude;								-- return coordinates from value_t
	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

	return string.format ('[[%s]]', title[args_t[1]]);
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 >----------------------------------------------------------------

]]

local function extents (lat, lon) -- Calculates bounding boxes of 1:50,000 and 1:250,000 scale map sheets/areas
	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
	

--[[--------------------------< G R I D >----------------------------------------------------------------------

This function takes latitude and longitude as input and calculates the Canadian National Tiling System ID of the
map sheet that contains that location.  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|lat=<number>|lon=<number>}} -- For 1:50,000 scale map sheet ID, using coordinates specified in argument

positional 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
--	for k, v in pairs (args_t) do
--		if 'number' == type (k) then											-- positional parameters only
--			if 'area' == v:lower() then											-- when the positional parameter has the value 'area' (case insentitive)
--				print_area = true;												-- render 1:250,000 scale map ('30M'); 1:50,000 scale map else ('30M11')
--			end
--			if 'link' == v:lower() then											-- when the positional parameter has the value 'link' (case insentitive)
--				print_link = true;												-- render output as a url to Geospatial Data Extraction tool
--			end
--			if 'name' == v:lower() then											-- when the positional parameter has the value 'name' (case insentitive)
--				print_name = true;												-- render output with name from ~/data appended
--			end
--		end
--	end

	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 '<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 '<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]

	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

	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]';
		local extents_t = extents (lat, lon);									-- get a table of sheet and area extents
		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,
}