# Module:Infobox dim

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 38 degrees N, given en-WP bias towards NA and Europe  
-- 38N was chosen because:  
-- * It's the furthest north where scale=100000 maps to zoom=12  
-- * One hemisphere (20000km) maps to zoom=1  
local metersPerPixelLevel9 = 305.748*math.cos(math.rad(38))  

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

-- 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 t = args.type  
if not t then return end  
local typeDim = mw.loadData('Module:Infobox_dim/data')  
local dim = typeDim[t]  
local population = tonumber(args.population)  
if t == '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 oddShape 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 (OSM zoom levels 1-17)  
if scale < 1600 then  
scale = 1600  
end  
if scale > 200e6 then  
scale = 200e6  
end  
return scale  
end  

-- Argument checking  
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  

-- 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 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  
return p._dim(getArgs) or ''  
end  

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

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

return p