Jump to content

Module:Infobox dim

Permanently protected module
From Wikipedia, the free encyclopedia

require('strict')
local getArgs = require('Module:Arguments').getArgs
local p = {}

local log2 = 0.693147181
local ppm = 1000/0.3  -- pixels per meter, from 0.3 mm / pixel from https://wiki.openstreetmap.org/wiki/Zoom_levels

-- To convert to OSM zoom level, we need to know meters per pixel at zoom level 9
-- On the equator, it's 305.748 meters/pixel according to https://wiki.openstreetmap.org/wiki/Zoom_levels
-- This quantity depends on the latitude (which we don't have easy access to)
-- Instead, we'll be correct at 30N, cos(30 degrees) = sqrt(3)/2
local metersPerPixelLevel9 = 305.748*math.sqrt(3)/2

-- Convert from Geohack's scale to OSM style zoom levels as used by <maplink>
local function geohackScaleToMapZoom(scale)
	scale = tonumber(scale)
	if not scale or scale <= 0 then return end
	return math.log(metersPerPixelLevel9*ppm/scale)/log2 + 9
end

local positiveNumericArgs = {viewport_cm=true,viewport_px=true,length_mi=true,length_km=true,
                             width_mi=true,width_km=true,area_mi2=true,area_km2=true,
                             area_acre=true,area_ha=true,scale=true,population=true}

local function cleanArgs(args)
    local clean = {}
    if type(args) == 'table' then
        for k, v in pairs(args) do
            if positiveNumericArgs[k] then
                v = v and mw.ustring.gsub(v,",","") -- clean out any commas
                v = tonumber(v)                     -- ensure argument is numeric
                if v and v <= 0 then                -- if non-positive, ignore value
                    v = nil
                end
            end
            clean[k] = v
         end
    end
    return clean
end

-- compute the viewport size (on screen) in meters, assuming ppm pixels per  meter on screen
local function computeViewport(args)
	local viewport_cm = tonumber(args.viewport_cm)
	local viewport_px = tonumber(args.viewport_px)
	return viewport_cm and viewport_cm / 100 or viewport_px and viewport_px / ppm
	       or tonumber(args.default_viewport) or 0.1
end

-- convert from geohack dim (knowing the viewpoint size on screen) to geohack scale
local function geohackDimToScale(dim, args)
	dim = tonumber(dim)
	args = args or {}
	if not dim or dim <= 0 then return end
	local units = args.units
	if units and string.lower(units) == 'km' then
		dim = dim*1000
	end
	return dim / computeViewport(args)
end

-- inverse of above function, returning dim in km
local function geohackScaleToDim(scale, args)
	scale = tonumber(scale)
	args = args or {}
	if not scale or scale <= 0 then return end
	return scale * computeViewport(args) * 1e-3
end

local oddShape = 2.09 --- length/sqrt(area) of Boston (to choose an example)

-- Convert from Geohack's types to Geohack dim
local function geohackTypeToDim(args)
	local type = args.type
	local typeDim = mw.loadData('Module:Infobox_dim/sandbox/data')
	local dim = typeDim[type]
	local population = tonumber(args.population)
	if type == 'city' and population and population > 0 then
		-- assume city is a circle with density of 1000/square kilometer
		-- compute diameter, in meters. Then multiply by 1.954 to account for weird shapes
		dim = 35.68e-3*math.sqrt(population)*oddShape
		-- don't zoom in too far
		if dim < 5 then
			dim = 5
		end
	end
	return dim		
end

-- Convert from dimension of object to Geohack dim
local function computeDim(length,width,area)
	if length and width then
		return math.max(length,width)
	end
	if length then return length end
	if width then return width end
	if area then return oddShape*math.sqrt(area) end
end

-- compute geohack dim from unit arguments (e.g., length_mi)
local function convertDim(args)
	local length = args.length_mi and 1.60934*args.length_mi or args.length_km
	local width = args.width_mi and 1.60934*args.width_mi or args.width_km
	local area = args.area_acre and 0.00404686*args.area_acre or 
		args.area_ha and 0.01*args.area_ha or 
		args.area_mi2 and 2.58999*args.area_mi2 or args.area_km2
	local dim = computeDim(length, width, area)
	return dim
end

local function computeScale(args)
	if args.scale then return args.scale end
	local dim, units, scale
	if args.dim then
		dim, units = mw.ustring.match(args.dim,"^([-%d%.]+)%s*(%D*)")
		args.units = units
		args.default_viewport = 0.1  -- default geohack viewpoirt
		scale = geohackDimToScale(dim, args)
	end
	if not scale then
		dim = convertDim(args) or geohackTypeToDim(args)
		args.units = 'km'
		args.default_viewport = 0.2 --- when object dimensions or type is specified, assume 20cm viewport
		scale = dim and geohackDimToScale(dim, args)
	end
	if not scale then return end
	scale = math.floor(scale+0.5)
	-- keep scale within sane bounds
	if scale < 2000 then
		scale = 2000
	end
	if scale > 250e6 then
		scale = 250e6
	end
	return scale
end

-- Module entry points
function p._dim(args)
    args = cleanArgs(args)
	if args.dim then return args.dim end
	-- compute scale for geohack
	local scale = args.scale
	local dim
	if not scale then
		args.default_viewport = 0.2 -- when specifying a object dimension or type, assume output spans 20cm
		dim = convertDim(args) or geohackTypeToDim(args)
		args.units = 'km'
		scale = dim and geohackDimToScale(dim, args)
	end
	-- reset back to 10cm viewport for correct geohack dim output
	args.viewport_cm = 10
	dim = scale and geohackScaleToDim(scale, args)
	return dim and tostring(math.floor(dim+0.5))..'km'
end

function p._scale(args)
    args = cleanArgs(args)
    return computeScale(args)
end

function p._zoom(args)
    args = cleanArgs(args)
	args.viewport_px = args.viewport_px or 200 --- viewport for Kartographer is 200px high
	local scale = computeScale(args)
	if scale then
		local zoom = geohackScaleToMapZoom(scale)
		return zoom and math.floor(zoom)
	end
end		

-- Template entry points
function p.dim(frame)
	return p._dim(getArgs(frame)) or ''
end

function p.scale(frame)
	return p._scale(getArgs(frame)) or ''
end

function p.zoom(frame)
	return p._zoom(getArgs(frame)) or ''
end

return p