Module:Coordinates/sandbox

From Wikimedia Commons, the free media repository
Jump to: navigation, search
--[[
 
This module is intended to provide functionality of {{location}} and related
templates. 
 
Please do not modify this code without applying the changes first at Module:Coordinates/sandbox and testing 
at Module:Coordinates/sandbox/testcases and Module talk:Coordinates/sandbox/testcases.
 
Authors and maintainers:
* User:Jarekt 
 
Functions:
*function coordinates.LocationTemplateCore(frame)
**function coordinates.GeoHack_link(frame)
***function coordinates.lat_lon(frame)
****function coordinates._deg2dms(deg,lang)
***function coordinates.externalLink(frame)
****function coordinates._externalLink(site, globe, latStr, lonStr, lang, attributes)
**function coordinates._getHeading(attributes)
**function coordinates.externalLinksSection(frame)
***function coordinates._externalLink(site, globe, latStr, lonStr, lang, attributes)
*function coordinates.getHeading(frame)  
*function coordinates.deg2dms(frame)
 
]]
 
coordinates = {};
 
-- =======================================
-- === Dependencies ======================
-- =======================================
local i18n     = require('Module:I18n/coordinates')    -- get localized translations of site names
local Fallback = require('Module:Fallback')            -- get fallback functions
local yesno    = require('Module:Yesno')
 
-- =======================================
-- === Hardwired parameters ==============
-- =======================================
 
-- Angles associated with each abriviation of compass point names. See [[:en:Points of the compass]]
local compass_points = {
  N    = 0,
  NBE  = 11.25,
  NNE  = 22.5,
  NEBN = 33.75,
  NE   = 45,
  NEBE = 56.25,
  ENE  = 67.5,
  EBN  = 78.75,
  E    = 90,
  EBS  = 101.25,
  ESE  = 112.5,
  SEBE = 123.75,
  SE   = 135,
  SEBS = 146.25,
  SSE  = 157.5,
  SBE  = 168.75,
  S    = 180,
  SBW  = 191.25,
  SSW  = 202.5,
  SWBS = 213.75,
  SW   = 225,
  SWBW = 236.25,
  WSW  = 247.5,
  WBS  = 258.75,
  W    = 270,
  WBN  = 281.25,
  WNW  = 292.5,
  NWBW = 303.75,
  NW   = 315,
  NWBN = 326.25,
  NNW  = 337.5,
  NBW  = 348.75,
}
 
-- URL definitions for different sites. Strings: $lat, $lon, $lang, $attr, $page will be 
-- replaced with latitude, longitude, language code, GeoHack attribution parameters and full-page-name strings.
local SiteURL = {
	GeoHack        = '//tools.wmflabs.org/geohack/geohack.php?pagename=$page&params=$lat_N_$lon_E_$attr&language=$lang',
	GoogleEarth    = '{{fullurl:toollabs:geocommons/earth.kml|latdegdec=$lat&londegdec=$lon&scale=10000&commons=1}}',
	Proximityrama  = '{{fullurl:toollabs:geocommons/proximityrama|latlon=$lat,$lon}}',
	OpenStreetMap  = '{{fullurl:toollabs:wiwosm/osm-on-ol/commons-on-osm.php|zoom=16&lat=$lat&lon=$lon}}',
	GoogleMaps = { 
		Mars  = '//www.google.com/mars/#lat=$lat&lon=$lon&zoom=8',
		Moon  = '//www.google.com/moon/#lat=$lat&lon=$lon&zoom=8',
		Earth = 'https://maps.google.com/maps?ll=$lat,$lon&spn=0.01,0.01&t=k&q=http://tools.wmflabs.org/geocommons/web.kml&hl=$lang'
	}
}
 
-- Categories
local CoorCat = {
	File          = '[[Category:Media with locations]]',
	Gallery       = '[[Category:Galleries with coordinates]]',
	Category      = '[[Category:Categories with coordinates]]',
	globe         = '[[Category:Media with %s locations]]',
	default       = '[[Category:Media with default locations]]',
	attribute     = '[[Category:Media with erroneous geoloaction attributes]]',
	erroneous     = '[[Category:Media with erroneous locations]]<span style="color:red;font-weight:bold">Error: Invalid parameters!</span>\n'
}
 
local NoLatLonString = 'latitude, longitude'
 
-- =======================================
-- === Functions =========================
-- =======================================
 
-- parse attribute variable returning desired field
function coordinates.parseAttribute(frame)
  return string.match(mw.text.decode(frame.args[1]), mw.text.decode(frame.args[2]) .. ':' .. '([^_]*)') or ''
end
 
--[[============================================================================
Parse attribute variable returning heading field. If heading is a string than 
try to convert it to an angle
==============================================================================]]
function coordinates.getHeading(frame)  
	local attributes
	if frame.args[1] then
		attributes = frame.args[1]
	elseif frame.args.attributes then
		attributes = frame.args.attributes
	else
		return ''
	end
	local hNum = coordinates._getHeading(attributes)
	if hNum == nil then
		return ''
	end
	return tostring(hNum)
end
-- Helper core function for getHeading. 
function coordinates._getHeading(attributes)
	if attributes == nil then
		return nil
	end
	local hStr = string.match(mw.text.decode(attributes), 'heading:([^_]*)')
	if hStr == nil then
		return nil
	end
	local hNum = tonumber( hStr )
	if hNum == nil then
		hStr = string.upper (hStr)
		hNum = compass_points[hStr]  
	end
	if hNum ~= nil then
		hNum = hNum%360
	end
	return hNum
end
 
--[[============================================================================
Convert degrees to degrees/minutes/seconds notation comonly used when displaying 
coordinates.
Inputs:
1) latitude or longitude angle in degrees
2) georeference precission in degrees
3) language used in formating of the number
==============================================================================]]
function coordinates.deg2dms(frame)
	local deg     = tonumber(frame.args[1])
	local degPrec = tonumber(frame.args[2]) or 0-- precision in degrees
	local lang
	if frame.args.lang and mw.language.isSupportedLanguage(frame.args.lang) then 
		lang = frame.args.lang
	else -- get user's chosen language 
		lang = mw.message.new( "lang" ):plain()
	end
	if deg==nil then
		return frame.args[1];
	else
		return coordinates._deg2dms(deg, degPrec, lang)
	end
end
 
--[[============================================================================
Helper core function for deg2dms. deg2dms can be called by templates, while 
_deg2dms should be called from Lua.
Inputs:
* deg - positive coordinate in degrees
* degPrec - coordinate precission in degrees will result in different angle format
* lang - language to used when formating the number
==============================================================================]]
function coordinates._deg2dms(deg, degPrec, lang)
	local dNum, mNum, sNum, dStr, mStr, sStr, formatStr, secPrec, c, k
	local Lang = mw.language.new(lang)
 
	-- adjust number display based on precission
	secPrec = degPrec*3600.0                     -- coordinate precission in seconds
	if secPrec<0.05 then                         -- degPrec<1.3889e-05
		formatStr = '%s°&nbsp;%s′&nbsp;%s″'      -- use DD° MM′ SS.SS″ format
		c = 360000
	elseif secPrec<0.5 then                      -- 1.3889e-05<degPrec<1.3889e-04
		formatStr = '%s°&nbsp;%s′&nbsp;%s″'      -- use DD° MM′ SS.S″ format
		c = 36000
	elseif degPrec*60.0<0.5 then                 -- 1.3889e-04<degPrec<0.0083
		formatStr = '%s°&nbsp;%s′&nbsp;%s″'      -- use DD° MM′ SS″ format
		c = 3600
	elseif degPrec<0.5 then                      -- 0.0083<degPrec<0.5
		formatStr = '%s°&nbsp;%s′'               -- use DD° MM′ format
		c = 60
	else -- if degPrec>0.5 then                  
		formatStr = '%s°'                        -- use DD° format
		c = 1
	end
 
	-- create degree, minute and seconds numbers and string
	d = c/60
	k  = math.floor(c*(deg%360)+0.49)     -- convert float to an integer. This step HAS to be identical for all conversions to avoid incorrect results due to different rounding
	dNum = math.floor(k/c) % 360          -- degree number (integer in 0-360 range)
	mNum = math.floor(k/d) %  60          -- minute number (integer in 0-60 range)
	sNum =      3600*(k%d) / c            -- seconds number (float in 0-60 range with 0, 1 or 2 decimal digits)
	dStr = Lang:formatNum(dNum)           -- degree string 
	mStr = Lang:formatNum(mNum)           -- minute string 
	sStr = Lang:formatNum(sNum)           -- second string 
	if mNum<10 then
		mStr = '0' ..mStr                 -- pad with zero if a single digit
	end
	if sNum<10 then
		sStr = '0' ..sStr                 -- pad with zero if less than ten
	end
	return string.format(formatStr, dStr, mStr, sStr);
end
 
--[[============================================================================
Format coordinate location string, by creating and joining DMS strings for 
latutude and longitude. Also convert precission from meters to degrees.
INPUTS:
 * lat        = latitude in degrees
 * lon        = longitude in degrees
 * lang       = language code
 * prec       = geolocation precission in meters
==============================================================================]]
function coordinates.lat_lon(frame)
	local lat  = tonumber(frame.args.lat)
	local lon  = tonumber(frame.args.lon)
	local prec = math.abs(tonumber(frame.args.prec) or 0) -- location precision in meters
	if lon then -- get longitude t0 be in -180 to 180 range
		lon=lon%360
		if lon>180 then
			lon = lon-360
		end
	end
	local lang
	if frame.args.lang and mw.language.isSupportedLanguage(frame.args.lang) then 
		lang = frame.args.lang
	else -- get user's chosen language 
		lang = mw.message.new( "lang" ):plain()
	end
	if lat==nil or lon==nil then
		return NoLatLonString
	else
		local nsew = Fallback._langSwitch(i18n.NSEW, lang) -- find set of localized translation of N, S, W and E in the desired language 
		local SN, EW, latStr, lonStr, lon2m, lat2m, phi
		if lat<0 then SN = nsew.S else SN = nsew.N end              -- choose S or N depending on latitude  degree sign
		if lon<0 then EW = nsew.W else EW = nsew.E end              -- choose W or E depending on longitude degree sign
		lat2m=1
		lon2m=1
		if prec>0 then -- if user specified the precission of the geo location...
			phi   = math.abs(lat)*math.pi/180   -- latitude in radiants
			lon2m = 6378137*math.cos(phi)*math.pi/180  -- see https://en.wikipedia.org/wiki/Longitude
			lat2m = 111000  -- average latitude degree size in meters
		end
		latStr = coordinates._deg2dms(math.abs(lat), prec/lat2m, lang) -- Convert latitude  degrees to degrees/minutes/seconds
		lonStr = coordinates._deg2dms(math.abs(lon), prec/lon2m, lang) -- Convert longitude degrees to degrees/minutes/seconds
		return string.format('%s&nbsp;%s, %s&nbsp;%s', latStr, SN, lonStr, EW)
		--return string.format('<span class="latitude">%s %s</span>, <span class="longitude">%s %s</span>', latStr, SN, lonStr, EW)
	end
end
 
--[[============================================================================
Create URL for different sites.
INPUTS:
 * site       = Possinle sites: GeoHack, GoogleEarth, Proximityrama, 
                OpenStreetMap, GoogleMaps (for Earth, Mars and Moon)
 * globe      = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan, 
                Ganymede are also supported but are unused as of 2013.
 * lat        = latitude string or number
 * lon        = longitude string or number
 * lang       = language code
 * attributes = attributes to be passed to GeoHack
==============================================================================]]
function coordinates.externalLink(frame)
	args = frame.args
	if args.lang and mw.language.isSupportedLanguage(args.lang) then 
		lang = args.lang
	else -- get user's chosen language 
		lang = mw.message.new( "lang" ):plain()
	end
	local str = coordinates._externalLink(args.site or 'GeoHack', args.globe or 'Earth', args.lat, args.lon, lang, args.attributes or '')
	return frame:preprocess(str)
end
 
--[[============================================================================
Helper core function for externalLink. Create URL for different sites:
INPUTS:
 * site       = Possinle sites: GeoHack, GoogleEarth, Proximityrama, 
                OpenStreetMap, GoogleMaps (for Earth, Mars and Moon)
 * globe      = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan, 
                Ganymede are also supported but are unused as of 2013.
 * latStr     = latitude string or number
 * lonStr     = longitude string or number
 * lang       = language code
 * attributes = attributes to be passed to GeoHack
==============================================================================]]
function coordinates._externalLink(site, globe, latStr, lonStr, lang, attributes)
	local URLstr = SiteURL[site];
	if site == 'GoogleMaps' then
		URLstr = SiteURL.GoogleMaps[globe]
	elseif site == 'GeoHack' then
		attributes = string.format('globe:%s_%s', globe, attributes)
		local pageName = mw.uri.encode( mw.title.getCurrentTitle().prefixedText, 'WIKI' )
		pageName = mw.ustring.gsub( pageName, '%%', '%%%%')
		URLstr = mw.ustring.gsub( URLstr, '$attr', attributes)
		URLstr = mw.ustring.gsub( URLstr, '$page', pageName)
		URLstr = mw.ustring.gsub( URLstr, ' ', '_')
	end
	URLstr = mw.ustring.gsub( URLstr, '$lat' , latStr)
	URLstr = mw.ustring.gsub( URLstr, '$lon' , lonStr)
	URLstr = mw.ustring.gsub( URLstr, '$lang', lang)
	return URLstr
end
 
--[[============================================================================
Adjust GeoHack attributes depending on the template that calls it
INPUTS:
 * attributes = attributes to be passed to GeoHack
 * mode = set by each calling template
==============================================================================]]
function coordinates.alterAttributes(attributes, mode)
	-- indicate which template called it
	if mode=='camera' then                                   -- Used by {{Location}} and {{Location dec}}
		if string.find(attributes, 'type:camera')==nil then
			attributes = 'type:camera_' .. attributes
		end
	elseif mode=='object'or mode =='globe' then              -- Used by {{Object location}}
		if string.find(attributes, 'class:object')==nil then
			attributes = 'class:object_' .. attributes
		end
	elseif mode=='inline' then                               -- Used by {{Inline coordinates}} (actually that template does not set any attributes at the moment)
	elseif mode=='user' then                                 -- Used by {{User location}}
		attributes = 'type:user_location'
	elseif mode=='institution' then                          --Used by {{Institution/coordinates}} (categories only)	
		attributes = 'type:institution'
	end
	return attributes
end
 
--[[============================================================================
 Create link to GeoHack tool which displays latitude and longitude coordinates 
 in DMS format
 INPUTS:
 * globe      = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan, 
                Ganymede are also supported but are unused as of 2013.
 * lat        = latitude in degrees
 * lon        = longitude in degrees
 * lang       = language code
 * prec       = geolocation precission in meters
 * attributes = attributes to be passed to GeoHack
==============================================================================]]
function coordinates.GeoHack_link(frame)
	-- create link and coordintate string
	local latlon = coordinates.lat_lon(frame)
	if latlon==NoLatLonString then
		return latlon
	else
		frame.args.site = 'GeoHack'
		local url = coordinates.externalLink(frame)
		return string.format('<span class="plainlinksneverexpand">[%s %s]</span>', url, latlon) --<span class="plainlinks nourlexpansion">
	end
end
 
--[[============================================================================
 Create full external links section of {{Location}} or {{Object location}} 
 templates, based on:
 * globe      = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan, Ganymede are also supported but are unused as of 2013.
 * mode       = Possible options: 
  - camera - call from {{location}}
  - object - call from {{Object location}}
  - globe  - call from {{Globe location}}
 * lat        = latitude in degrees
 * lon        = longitude in degrees
 * lang       = language code
 * namespace  = namespace name: File, Category, (Gallery)
==============================================================================]]
function coordinates.externalLinksSection(frame)
	args = frame.args
	if args.lang and mw.language.isSupportedLanguage(args.lang) then 
		lang = args.lang
	else -- get user's chosen language 
		lang = mw.message.new( "lang" ):plain()
	end
	if not args.namespace then
		args.namespace = mw.title.getCurrentTitle().namespace
	end
 
	local str
	if args.globe=='Earth' then -- Earth locations will have 3 or 4 links
		str = string.format('[%s %s] - [%s %s] - [%s %s]', 
			coordinates._externalLink('OpenStreetMap', 'Earth', args.lat, args.lon, lang, ''),  
			Fallback._langSwitch(i18n.OpenStreetMaps, lang),
			coordinates._externalLink('GoogleMaps'   , 'Earth', args.lat, args.lon, lang, ''),  
			Fallback._langSwitch(i18n.GoogleMaps, lang),
			coordinates._externalLink('GoogleEarth'  , 'Earth', args.lat, args.lon, lang, ''),  
			Fallback._langSwitch(i18n.GoogleEarth, lang))
		if args.namespace=="Category" then
			str = string.format('%s - [%s %s]', str,
				coordinates._externalLink('Proximityrama', 'Earth', args.lat, args.lon, lang, ''),  
				Fallback._langSwitch(i18n.Proximityrama, lang))
		end 
	elseif args.globe=='Mars' or args.globe=='Moon' then
		str = string.format('[%s %s]', 
			coordinates._externalLink('GoogleMaps', args.globe, args.lat, args.lon, lang, ''),  
			Fallback._langSwitch(i18n.GoogleMaps, lang))
	end
 
	return frame:preprocess(str) -- use preprocess to expand {{#fullurl}}
	--return str
end
 
--[[============================================================================
Core section of template:Location, template:Object location and template:Globe location.
This method requires several arguments to be passed to it or it's parent metchod/template:
 * globe      = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan, Ganymede are also supported but are unused as of 2013.
 * mode       = Possible options: 
  - camera - call from {{location}}
  - object - call from {{Object location}}
  - globe  - call from {{Globe location}}
 * lat        = latitude in degrees
 * lon        = longitude in degrees
 * attributes = attributes
 * lang       = language code
 * namespace  = namespace: File, Category, Gallery
 * prec       = geolocation precission in meters
==============================================================================]]
function coordinates.LocationTemplateCore(frame)
	-- prepare arguments
	args = frame.args
	if not args or not args.lat then -- if no arguments provided than use parent arguments
		args = mw.getCurrentFrame():getParent().args
	end
	if not (args.lang and mw.language.isSupportedLanguage(args.lang)) then 
		args.lang = mw.message.new( "lang" ):plain() -- get user's chosen language 
	end
	if not (args.namespace) then -- if namespace not provided than look it up
		args.namespace = mw.title.getCurrentTitle().namespace
	end
	if args.namespace=='' then -- if empty than it is a gallery
		args.namespace = 'Gallery'
	end
	local bare   = yesno(args.bare,false)
	local Status = 'primary' -- used by {{#coordinates:}}
	if yesno(args.secondary,false) then
		Status = 'secondary'
	end
	args.attributes = coordinates.alterAttributes(args.attributes or '', args.mode)
	frame.args = args
 
	-- check for errors and add Geo (microformat) code for machine readability.
	local lat = tonumber(args.lat)
	local lon = tonumber(args.lon)
	if lon then -- get longitude t0 be in -180 to 180 range
		lon=lon%360
		if lon>180 then
			lon = lon-360
		end
	end
	local Categories, geoMicroFormat, coorTag = '', '', ''
 
	-- Categories, {{#coordinates}} and geoMicroFormat will be only added to File, Category and Gallery pages
	if (args.namespace == 'File' or args.namespace == 'Category' or args.namespace == 'Gallery') then
		if lat and lon then -- if lat and lon are numbers...
			if lat==0 and lon==0 then -- lat=0 and lon=0 is a common issue when copying from flickr and other sources
				Categories = CoorCat.default
			end
			if frame.args.attributes and string.find(frame.args.attributes, '=') then
				Categories = Categories .. CoorCat.attribute
			end
			if args.noError==0 or (math.abs(lat)>90) then -- check for errors ({{#coordinates:}} also checks for errors )
				Categories = Categories .. CoorCat.erroneous
			end
			local cat = CoorCat[args.namespace]
			if cat then -- add category based on namespace
				Categories = Categories .. cat
			end
			-- if not earth than add a category for each globe
			if args.mode and args.globe and args.mode=='globe' and args.globe~='Earth' then
				Categories = Categories .. string.format(CoorCat[args.mode], args.globe)
			end
			-- add  <span class="geo"> Geo (microformat) code: it is included for machine readability
			geoMicroFormat = string.format('<span class="geo" style="display:none">%10.6f; %11.6f</span>',lat, lon)
			-- add {{#coordinates}} tag, see https://www.mediawiki.org/wiki/Extension:GeoData
			if args.namespace == 'File' and Status == 'primary' and args.mode=='camera' then 
				coorTag = string.format('{{#coordinates:primary|%10.6f|%11.6f|%s}}', lat, lon, args.attributes)
			end
		else -- if lat and lon are not numbers then add error category
			Categories = Categories .. CoorCat.erroneous
		end
 
	end
 
	-- Call helper functions to render different parts of the template
	local str1, str2, str3, str4, inner_table, heading
	str1 = coordinates.GeoHack_link(frame)  									-- the coordinates and link to GeoHack
	heading = coordinates._getHeading(frame.args.attributes)					-- get heading arrow section
	if heading then
		--str1 =  string.format('%s&nbsp;&nbsp;<span style="{{Transform-rotate|%f}}">[[File:North Pointer.svg|20px|link=|alt=]]</span>', str1, 360-heading)
		local fname = string.format('{{Compass rose file|%f|style=heading}}', heading)
		str1 = string.format('%s&nbsp;&nbsp;<span title="%s°">[[%s|25px|link=|alt=%s°]]</span>', str1, heading, fname, heading)
	end
	str2 = Fallback._langSwitch(i18n.LocationTemplateLinkLabel, args.lang) 	-- header of the link section
	str3 = coordinates.externalLinksSection(frame) or ''					-- external link section
	str4 = '[[File:Circle-information.svg|18x18px|alt=info|link=Commons:Geocoding]]'
	inner_table = string.format('<td style="border:none;">%s</td><td style="border:none;">%s %s</td><td style="border:none;">%s%s</td>', str1, str2, str3, str4, geoMicroFormat)
 
	-- combine strings into a table
	local templateText
	if bare then
		templateText  = string.format('<table style="width:100%%"><tr>%s</tr></table>', inner_table)
	else
		-- choose name of the field
		local field_name = 'Location'
		if args.mode=='camera' then 
			field_name = Fallback._langSwitch(i18n.CameraLocation, args.lang)
		elseif args.mode=='object' then 
			field_name = Fallback._langSwitch(i18n.ObjectLocation, args.lang)
		elseif args.mode=='globe' then
			field_list = Fallback._langSwitch(i18n.GlobeLocation, args.lang)
			if args.globe and i18n.GlobeLocation['en'][args.globe] then -- verify globe is provided and is recognized
				field_name = field_list[args.globe]
			end
		end
		local style = frame:expandTemplate{ title="Infobar-Layout", args={ ["lang"] = lang, ["class"] = 'commons-file-information-table' } }
		templateText  = string.format('<table %s><tr><th class="type fileinfo-paramfield">%s</th>%s</tr></table>', style, field_name, inner_table)
	end
	return frame:preprocess(templateText .. Categories .. coorTag)
end
 
return coordinates