Module:Coordinates

Under construction icon-blue.svg

This page is under construction, or in the middle of an expansion or major redevelopment.
You are welcome to assist in its construction by editing it as well. For information on its status, please contact Jarekt.

This module is intended to (eventually) replace the functionality of {{location}} and related templates, but at the moment it is collection of methods related to geolocation. It provides several methods most of them not used yet:

  • {{#Invoke:Coordinates | parseAttribute| attribute string | attribute name }} : parse {{location}} attribute parameter attribute string and return value of the attribute name parameter
  • {{#Invoke:Coordinates | getHeader| attribute string }} : parse {{location}} attribute parameter attribute string and return the numeric value of the header attribute. Careful, this function is live (called by {{Location/layout}}).
  • {{#Invoke:Coordinates | GeoHack_link| lat=... | lon=... |lang=xx | site=... | globe=... }}: creates link to GeoHack tool and display location coordinates. The URLs are based on website and latitude/longitude coordinates. Language is used so it can be passes to the website. Globe parameter is used to allow specifying coordinates on planets other than earth. Careful, this function is live (called by {{Institution/coordinates}}).
  • {{#Invoke:Coordinates | lat_lon| lat=... | lon=... |lang=xx }}: create coordinate location string based on decimal degrees latitude and longitude number. Language is used to localize the presentation of the numbers.
  • {{#Invoke:Coordinates | deg2dms| degrees|lang=xx }}: create dms (degree/minute/second) string based on decimal degrees number. Language is used to localize the presentation of the numbers.
  • {{#Invoke:Coordinates | externalLink| site=... | globe=... | lat=... | lon=... |lang=xx }}: create URLs for different sites which are used by coordinate location templates. The URLs are based on website and latitude/longitude coordinates. Language is used so it can be passes to the website. Globe parameter is used to allow specifying coordinates on planets other than earth.

Examples

  • {{#invoke:Coordinates| GeoHack_link | lat=51.48 | lon=0}} will display 51° 28′ 48″ N, 0° 00′ 00″ E
  • {{#invoke:Coordinates| GeoHack_link | lat=51.48 | lon=0 | lang=en }} will display 51° 28′ 48″ N, 0° 00′ 00″ E
  • {{#invoke:Coordinates| GeoHack_link | lat=51.48 | lon=0 | lang=ru }} will display 51° 28′ 48″ с. ш., 0° 00′ 00″ в. д.
  • {{#invoke:Coordinates| GeoHack_link | lat= | lon=0 | lang=ru }} (with missing latitude value) will display "latitude, longitude"
  • {{#invoke:Coordinates| externalLinksSection | globe = Earth| lat=51.48 | lon=0 | lang=en | namespace=File}} displays "[{{fullurl:tools:~kolossos/openlayers/commons-on-osm.php|zoom=16&lat=51.48&lon=0}} OpenStreetMap] - Google Maps - [{{fullurl:tools:~para/GeoCommons/earth.php|latdegdec=51.48&londegdec=0&scale=10000&commons=1}} Google Earth]"
  • {{#invoke:Coordinates| externalLinksSection | globe = Earth| lat=51.48 | lon=0 | lang=en | namespace=Category}} displays "[{{fullurl:tools:~kolossos/openlayers/commons-on-osm.php|zoom=16&lat=51.48&lon=0}} OpenStreetMap] - Google Maps - [{{fullurl:tools:~para/GeoCommons/earth.php|latdegdec=51.48&londegdec=0&scale=10000&commons=1}} Google Earth]"
  • {{#invoke:Coordinates| externalLinksSection | globe = Earth| lat=51.48 | lon=0 | lang=ru | namespace=Category}} displays "[{{fullurl:tools:~kolossos/openlayers/commons-on-osm.php|zoom=16&lat=51.48&lon=0}} OpenStreetMap] - Картах Google - [{{fullurl:tools:~para/GeoCommons/earth.php|latdegdec=51.48&londegdec=0&scale=10000&commons=1}} Google Планете Земля]"
  • {{#invoke:Coordinates| externalLinksSection | globe = Mars| lat=51.48 | lon=0 | lang=en | namespace=File}} displays "Google Maps"
  • {{#invoke:Coordinates| externalLinksSection | globe = Moon| lat=51.48 | lon=0 | lang=en | namespace=File}} displays "Google Maps"
  • {{#invoke:Coordinates| LocationTemplateCore | globe = Earth| lat=51.48 | lon=0 | lang=en | namespace=File| attributes=elevation:10_heading:W | mode=camera | bare = 1}} displays "
    51° 28′ 48″ N, 0° 00′ 00″ E  View this and other nearby images on: OpenStreetMap - Google Maps - Google Earthinfo
    {{#coordinates: 51.480000| 0.000000|type:camera_elevation:10_heading:W}}"
  • {{#invoke:Coordinates| LocationTemplateCore | globe = Earth| lat=51.48 | lon=0 | lang=en | namespace=File| attributes=elevation:10_heading:W | mode=camera | bare = 0}} displays "
    Camera location51° 28′ 48″ N, 0° 00′ 00″ E  View this and other nearby images on: OpenStreetMap - Google Maps - Google Earthinfo
    {{#coordinates: 51.480000| 0.000000|type:camera_elevation:10_heading:W}}"

See testcases to see more examples.

Dependencies

Relies on Module:I18n/coordinates for all of the internationalization translations.


--[[

This module is intended to (eventually) replace some or all functionality of {{location}} and related
templates. At the moment it is collection of methods related to geolocation. 

*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 will be replaced with latitude, longitude, language code and GeoHack attribution parameters strings.
local SiteURL = {
	GeoHack        = 'http://tools.wmflabs.org/geohack/geohack.php?pagename={{FULLPAGENAMEE}}&params=$lat_N_$lon_E_$attr&language=$lang',
	GoogleEarth    = '{{fullurl:tools:~para/GeoCommons/earth.php|latdegdec=$lat&londegdec=$lon&scale=10000&commons=1}}',
	Proximityrama  = '{{fullurl:tools:~para/GeoCommons/proximityrama|latlon=$lat,$lon}}',
	OpenStreetMap  = '{{fullurl:tools:~kolossos/openlayers/commons-on-osm.php|zoom=16&lat=$lat&lon=$lon}}',
	GoogleMaps = { 
		Mars  = 'http://www.google.com/mars/#lat=$lat&lon=$lon&zoom=8',
		Moon  = 'http://www.google.com/moon/#lat=$lat&lon=$lon&zoom=8',
		Earth = 'http://maps.google.com/maps?ll=$lat,$lon&spn=0.01,0.01&t=k&q=http://toolserver.org/~para/GeoCommons/GeoCommons-simple.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]]',
	erroneous     = '[[Category:Media with erroneous locations]]<span style="color:red;font-weight:bold">Error: Invalid parameters!</span>\n'
}

-- =======================================
-- === 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
function coordinates.deg2dms(frame)
	local deg = tonumber(frame.args[1])
	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 = frame:preprocess( "{{int:lang}}" )
	end
	if deg==nil then
		return frame.args[1];
	else
		return coordinates._deg2dms(deg,lang)
	end
end
-- Helper core function for deg2dms. 
function coordinates._deg2dms(deg,lang)
	local dNum, mNum, sNum, dStr, mStr, sStr
	local Lang = mw.language.new(lang)
	deg  = math.floor(360000*(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(deg/360000) % 360          -- degree number (integer in 0-360 range)
	mNum = math.floor(deg/6000  ) %  60          -- minute number (integer in 0-60 range)
	sNum =           (deg%6000  ) / 100          -- seconds number (float with 2 decimal digits in 0-60 range)
	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
	str = string.format('%s°&nbsp;%s′&nbsp;%s″', dStr, mStr, sStr);
	return str
end

-- format coordinate location string 
function coordinates.lat_lon(frame)
	local lat = tonumber(frame.args.lat)
	local lon = tonumber(frame.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 lang
	if frame.args.lang and mw.language.isSupportedLanguage(frame.args.lang) then 
		lang = frame.args.lang
	else -- get user's chosen language 
		lang = frame:preprocess( "{{int:lang}}" )
	end
	if lat==nil or lon==nil then
		return 'latitude, longitude'
	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
		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
		latStr = coordinates._deg2dms(math.abs(lat), lang)     -- Convert latitude  degrees to degrees/minutes/seconds
		lonStr = coordinates._deg2dms(math.abs(lon), 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 based on globe (planet), latitude, longitude, language code and GeoHack attribution parameters
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 = frame:preprocess( "{{int:lang}}" )
	end
	return coordinates._externalLink(args.site or 'GeoHack', args.globe or 'Earth', args.lat, args.lon, lang, args.attributes or '')
end
-- Helper core function for externalLink 
function coordinates._externalLink(site, globe, latStr, lonStr, lang, attributes)
	local str
	if site == 'GoogleMaps' then
		str = SiteURL.GoogleMaps[globe]
	else
		str = SiteURL[site];
		attributes = string.format('globe:%s_%s', globe, attributes)
		str = mw.ustring.gsub( str, '$attr', attributes)
	end
	str = mw.ustring.gsub( str, '$lat', latStr)
	str = mw.ustring.gsub( str, '$lon', lonStr)
	str = mw.ustring.gsub( str, '$lang', lang)
	return str
end

-- adjust attributes depending on the template that calls it
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
function coordinates.GeoHack_link(frame)
	-- create link and coordintate string
	local latlon = coordinates.lat_lon(frame)
	if latlon=='lattiude, longitude' then
		return latlon
	else
		frame.args.site = 'GeoHack'
		local url = frame:preprocess(coordinates.externalLink(frame)) -- use preprocess to get page name
		return string.format('<span class="plainlinksneverexpand">[%s %s]</span>', url, latlon) --<span class="plainlinks nourlexpansion">
	end
end

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 = frame:preprocess( "{{int:lang}}" )
	end
	if not args.namespaceNum then
		args.namespace = frame:preprocess( "{{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 name: File, Category, (Gallery)
]]
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 = frame:preprocess( "{{int:lang}}" ) -- get user's chosen language 
	end
	if not (args.namespace) then -- if not provided than look up
		args.namespace = frame:preprocess( "{{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 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)
			-- https://www.mediawiki.org/wiki/Extension:GeoData
			if args.namespace == 'File' and Status ~= 'secondary' then -- TODO enable for secondary cases without throwing errors
				--coorTag = string.format('{{#coordinates:%f|%f|%s|%s}}', frame.args.lat, frame.args.lon, args.attributes, Status)
				coorTag = string.format('{{#coordinates:%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;[[%s|25px|link=|alt=]]', str1, 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:preprocess(string.format('{{Infobar-Layout|lang=%s}}',lang))
		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
最終更新日時 2014年3月30日 (日) 23:14