Module:GlobeCoordinate

From Wikipedia

Documentation for this module may be created at Module:GlobeCoordinate/doc

local GlobeCoordinate = {}

--Internal functions
--[[
    Validate a GlobeCoordinate defintion
    @param table definition data
    @return boolean
]]--
function validate( definition )
    --Validate constantes
    if definition.globe ~= GlobeCoordinate.GLOBE.EARTH then
        return false
    end

    --Validate precision
    if not validateNumberInRange( definition.precision, 0, 1 ) then
        return false
    end

    --Validate latitude and longitude
    if not validateNumberInRange( definition.latitude, -180, 360 ) or not validateNumberInRange( definition.longitude, -180, 360 ) then
        return false
    end

    return true
end

--[[
    Check if a value is a number in the given range
    @param mixed value
    @param number min
    @param number max
    @return boolean
]]--
function validateNumberInRange( value, min, max )
    return type( value ) == 'number' and value >= min and value <= max
end

--[[
    Try to find the relevant precision for a GlobeCoordinate definition
    @param table GlobeCoordinate definition
    @return number the precision
]]--
function guessPrecision( definition )
    return math.max( detectPrecisionForFloat( definition.latitude ), detectPrecisionForFloat( definition.longitude ) )
end

--[[
    Try to find the relevant precision for a latitude or longitude as float
    @param float float
    @return number the precision
]]--
function detectPrecisionForFloat( float )
	local parts = mw.text.split( tostring( float ), '.' )
	if parts[2] then
		return math.pow( 10, -1 * #parts[2] )
	else
		return 1
	end
end

--[[
    Format a float coordinate as DMS according to the precision
    @param float float
    @param precision float
    @param positive string the tag if the coordinate is positive, like 'N'
    @param positive string the tag if the coordinate is negative, like 'S'
    @return string the coordinate in DMS format
]]--
function formatDMS( float, precision, positive, negative )
	local isNegative = float < 0
	float = math.abs( float )
	local d = math.floor( float )
	local dms = d .. '°'

	if precision <= 1/60 then
		float = (float - d) * 60
		local m = math.floor( float )
		dms = dms .. ' ' .. m .. '′'

		if precision <= 1/3600 then
			float = (float - m) * 60
			local s
			if float%2 ~= 0.5 then
			    s = math.floor( float + 0.5 )
			else
			    s = float - 0.5
			end
			dms = dms .. ' ' .. s .. '″'
			--TODO: precision higher than second
		end
	end

	if isNegative then
		return dms .. ' ' .. negative
	else
		return dms .. ' ' .. positive
	end
end

--Public interface
--[[
    Build a new GlobeCoordinate
    @param table definition definition of the coodinate
    @return GlobeCoordinate|nil
]]--
function GlobeCoordinate.new( definition )
    --Default values
    if definition.precision == nil then
        definition.precision = guessPrecision( definition )
    end
    if definition.globe == nil then
        definition.globe = GlobeCoordinate.GLOBE.EARTH
    end

    if not validate( definition ) then
        return nil
    end

    local coord = {
        latitude = definition.latitude,
        longitude = definition.longitude,
        globe = definition.globe or GlobeCoordinate.GLOBE.EARTH,
        precision = definition.precision or 0
    }

    setmetatable( coord, {
        __index = GlobeCoordinate,
        __tostring = function( self ) return self:toString() end
    } )
        
    return coord
end

--[[
    Build a new GlobeCoordinate from a Wikidata GlobeCoordinate value
    @param table wikidataValue the coordinate as represented by Wikidata
    @return GlobeCoordinate|nil
]]--
function GlobeCoordinate.newFromWikidataValue( wikidataValue )
    if  wikidataValue.globe == 'http://www.wikidata.org/entity/Q2' then
        wikidataValue.globe = GlobeCoordinate.GLOBE.EARTH
    else
        return nil
    end

    return GlobeCoordinate.new( wikidataValue )
end

--[[
    Return a GlobeCoordinate as a string
    @param mw.language|string|nil language to use. By default the content language.
    @return string
    @todo i18n
]]--
function GlobeCoordinate:toString( language )
    return formatDMS( self.latitude, self.precision, 'N', 'S' ) .. ' ' .. formatDMS( self.longitude, self.precision, 'E', 'W' )
end

--[[
    Return a GlobeCoordinate in HTMl (with a <GlobeCoordinate> node)
    @param mw.language|string|nil language to use. By default the content language.
    @param table|nil attributes table of attributes to add to the <GlobeCoordinate> node.
    @return string
]]--
function GlobeCoordinate:toHtml( language, attributes )
	return mw.text.tag(
		'span', {
				["class"] = "geo"
			},
		mw.text.tag( 'span', {
				["class"] = "latitude",
				["title"] = self.latitude,
			},
			formatDMS( self.latitude, self.precision, 'N', 'S' )
		) ..
		' ' ..
		mw.text.tag( 'span', {
				["class"] = "longitude",
				["title"] = self.longitude,
			},
			formatDMS( self.longitude, self.precision, 'E', 'W' )
		)
	)
end

--[[
    Supported globes
]]--
GlobeCoordinate.GLOBE = {
	EARTH = 'Earth'
}

return GlobeCoordinate