Modul:Location map

Die Dokumentation für dieses Modul kann unter Modul:Location map/Doku erstellt werden

-- documentation
local LocationMap = {
	suite  = 'Location map',
	serial = '2022-10-21',
	item   = 15934920
}

-- module import
-- require( 'strict' )
local cd = require( 'Module:Coordinates' )
local li = require( 'Module:Location map/i18n' )
local lp = require( 'Module:Location map/Params' )

-- module variable
local locMap = {
	maintenance = {}
}

-- Local functions, please do not call them directly

local function split( s )
	local tb = mw.text.split( s, ';' )
	for i = #tb, 1, -1 do
		tb[ i ] = mw.text.trim( tb[ i ] )
		if tb[ i ] == '' then
			table.remove( tb, i )
		end
	end
	return tb
end

local function getClass( style, list )
    local tb = split( style )
    local class, rule
    for i = #tb, 1, -1 do
        rule = tb[ i ]
        if list[ rule ] then
            class = list[ rule ]
            table.remove( tb, i )
        end
    end
    return class, table.concat( tb, '; ' )
end

local function addMaintenance( s )
	if s and s ~= '' then
		table.insert( locMap.maintenance, s )
	end
end

local function getMaintenance()
	local m = table.concat( locMap.maintenance, ' ' )
	if m ~= '' then
		m = '<span class="error">' .. m .. '</span>'
	end
	return m
end

local function isSet( s )
	if not s then
		return s
	end
	s = mw.text.trim( s )
	if s ~= '' then
		return s
	else
		return nil
	end
end

local function round( n )
	return math.floor( n * 100 + 0.5 ) / 100
end

local function setLocation( args )
	local lmarksize = math.floor( args.marksize + 0.5 )
	local msize = math.floor( ( args.marksize - 1 ) / 2 + 0.5 )
	local msize3 = math.floor( ( args.marksize + 2 ) / 2 + 0.5 )
	local msize5 = math.floor( ( args.marksize + 4 ) / 2 + 0.5 )
	local centerPosition = -msize .. 'px'

	-- create marker box
	local markerBox = mw.html.create( 'div' )
		:addClass( 'voy-locmap-marker-box' )
		:css( { top = round( args.y * 100 ) .. '%',
			left = round( args.x * 100 ) .. '%' } )

	-- add marker symbol
	if args.mark ~= 'none' then
		markerBox:node( mw.html.create( 'div' )
			:addClass( 'voy-locmap-marker-symbol' )
			:css( {
				top = centerPosition,
				left = centerPosition,
				[ 'min-width' ] = lmarksize .. 'px',
				[ 'min-height' ] = lmarksize .. 'px'
			} )
			:wikitext( mw.ustring.format( '[[File:%s|%sx%spx|top|class=noviewer notpageimage|link=%s|%s]]',
				args.mark, lmarksize, lmarksize, args.name, args.name) )
		)
	end

	-- add label
	local wrapClass, labelClass
	if args.label ~= '' and args.label ~= 'none' then
		if args.labelWrap == 'manual' then
			wrapClass = 'voy-locmap-marker-label-nowrap'
		end

		local styles = {}
		local pos = li.labelPositions[ args.labelPosition ]
		if pos then
			pos = pos:gsub( 'msize_', msize )
				:gsub( 'msize3_', msize3 )
				:gsub( 'msize5_', msize5 )
			table.insert( styles, pos )
		else
			-- estimation of posititon
			if args.y <= 0.5 then
				table.insert( styles, li.labelPositions.yTop:format( msize3 ) )
			else
				table.insert( styles, li.labelPositions.yBottom:format( msize3 ) )
			end
			if args.x < 0.25 then
				table.insert( styles,
					li.labelPositions.xLeft:format( math.floor( 3 - 60 * args.x ) / 10 ) )
			elseif args.x <= 0.75 then
				table.insert( styles, li.labelPositions.xCenter )
			else
				table.insert( styles,
					li.labelPositions.xRight:format( math.floor( 3 - 60 * ( 1 - args.x ) ) / 10 ) )
			end
		end
		labelClass, args.labelStyle = getClass( args.labelStyle, li.labelClasses )
		table.insert( styles, args.labelStyle )

		markerBox:node( mw.html.create( 'div' )
			:addClass( 'voy-locmap-marker-label' )
			:addClass( wrapClass )
			:addClass( labelClass )
			:cssText( isSet( table.concat( styles, ' ' ) ) )
			:node( mw.html.create( 'span' )
				:addClass( 'voy-locmap-marker-label-text' )
				:wikitext( args.label )
			)
		)
	end
	return tostring( markerBox )
end

local function baseMap( args )
	-- map and map container
	local map = mw.ustring.format( '[[File:%s|%spx|center|class=noviewer notpageimage|link=|%s]]',
		 args.mapImage, args.width, args.description )

	-- add marker
	if args.x < 0 or args.x > 1 or args.y < 0 or args.y > 1 then
		map = map .. tostring( mw.html.create( 'div' )
			:addClass( 'voy-locmap-error' )
			:wikitext( mw.ustring.format( li.errMsgs.coordError, args.name ) )
		)
	else
		map = map .. setLocation( args )
	end

	local style = ( args.caption ~= '' and args.captionInnerBorder ~= '' )
		and ( 'border:' .. args.captionInnerBorder ) or ''
	map = mw.html.create( 'div' )
		:addClass( 'voy-locmap-map-box' )
		:cssText( isSet( style ) )
		:wikitext( map .. args.places ) -- adding places to map

	-- add map caption
	local caption, class
	if args.caption ~= '' then
		caption = mw.html.create( 'div' )
			:addClass( 'thumbcaption voy-locmap-caption' )
			:cssText( isSet( args.captionStyle ) )
			:wikitext( args.caption )
	end

	-- create outer box
	class, style = getClass( args.mapStyle, li.mapClasses )
	class = isSet( class ) or 'voy-locmap-center'
	if args.caption ~= '' and args.captionOuterBorder ~= '' then
		style = style .. '; border:' .. args.captionOuterBorder
	end

	return tostring( mw.html.create( 'div' )
		:addClass( 'voy-locmap' )
		:addClass( class )
		:addClass( args.caption ~= '' and 'voy-locmap-with-caption' or nil )
		:cssText( isSet( style ) )
		:node( map )
		:node( caption )
	)
end

-- Handling regional map data
-- This function is never to be called directly but with a pcall()
-- to handle exceptions in case of missing map modules
local function getMapData( id )
	local region = require( li.modulePrefix .. id )
	if region then
		region.id = id
	end
	return region
end

local function linearX( mapData, long )
	local left = mapData.left
	local right = mapData.right
	if not mapData or not left or not right or left == right then
		return -1 -- error
	elseif left < right then
		return ( long - left ) / ( right - left )
	elseif long < 0 then
		return ( 360 + long - left ) / ( 360 + right - left )
	else
		return ( long - left ) / ( 360 + right - left )
	end
end

local function linearY( mapData, lat )
	local top = mapData.top
	local bottom = mapData.bottom
	if not mapData or not top or not bottom or top == bottom then
		return -1 -- error
	end
	return ( lat - top ) / ( bottom - top )
end

local function getX( mapData, long, lat )
	if mapData.x then
		return mapData.x( lat, long )
	else
		return linearX( mapData, long )
	end
end

local function getY( mapData, long, lat )
	if mapData.y then
		return mapData.y( lat, long )
	else
		return linearY( mapData, lat )
	end
end

local function getMapImage( mapData, which )
	local image = mapData.default
	if which == 'quickbar' then
		which = li.defaults.quickbarMapType
		if ( mapData.quickbar or '' ) ~= '' then
			which = mapData.quickbar
		end
	end
	if which ~= '' and ( mapData[ which ] or '' ) ~= '' then
		image = mapData[ which ]
	end
	return image
end

-- parameters handling
local function argCheck( param, altValue )
	if not param or param == '' then
		return altValue
	end
	param = mw.text.trim( param )
	if param == '' then
		param = altValue
	end
	return param
end

-- checking a set of arguments
local function argsCheck( args, keys )
	for i, key in ipairs( keys ) do
		args[ key ] = argCheck( args[ key ], '' )
	end
end

local function checkMarkerProperties( args, mapData )
	args.mark = argCheck( args.mark, mapData.mark or li.defaults.markerImg )
	args.marksize = argCheck( args.marksize, mapData.marksize or li.defaults.markerSize )

	argsCheck( args, { 'name', 'label', 'labelWrap', 'labelPosition', 'labelStyle',
		'labelBackground' } )

	if args.labelBackground ~= '' then
		args.labelBackground = 'background: ' .. args.labelBackground
		if args.labelStyle ~= '' then
			args.labelStyle = args.labelStyle .. '; ' .. args.labelBackground
		else
			args.labelStyle = args.labelBackground
		end
	end

	return args
end

local function checkCoordinate( args, mapData )
	local success = true
	local t
	args.lat = argCheck( tostring( args.lat ), '' )
	args.long = argCheck( tostring( args.long ), '' )
	if args.lat ~= '' and args.long ~= '' then
		t = tonumber( args.lat )
		if t then
			args.lat = math.abs( t ) <= 90 and t or ''
		else
			t = cd.toDec( args.lat, 'lat', 6 )
			args.lat = t.error == 0 and t.dec or ''
		end

		t = tonumber( args.long )
		if t then
			args.long = ( t > -180 and t <= 180 ) and t or ''
		else
			t = cd.toDec( args.long, 'long', 6 )
			args.long = t.error == 0 and t.dec or ''
		end
	end
	if args.lat == '' or args.long == '' then
		return -1, -1, false
	end

	local x = getX( mapData, args.long, args.lat )
	if x < 0 or x > 1 then
		success = false
		if x == -1 then
			addMaintenance( li.errMsgs.wrongXBorders )
		else
			addMaintenance( mw.ustring.format( li.errMsgs.wrongLong,
				tonumber( args.long ) or 0 ) ) 
		end
	end

	local y = getY( mapData, args.long, args.lat )
	if y < 0 or y > 1 then
		success = false
		if y == -1 then
			addMaintenance( li.errMsgs.wrongYBorders )
		else
			addMaintenance( mw.ustring.format( li.errMsgs.wrongLat,
				tonumber( args.lat ) or 0 ) )
		end
	end

	return x, y, success
end

local function checkParameters( args, list )
	local unknown = {}
	for key, value in pairs( args ) do
		if not list[ key ] then
			table.insert( unknown, "''" .. key .. "''" )
		end
	end
	local category = li.errMsgs.wrongParam
	if #unknown == 1 then
		addMaintenance( category
			.. mw.ustring.format( li.errMsgs.unknownParam, unknown[ 1 ] ) )
	elseif #unknown > 1 then
		addMaintenance( category .. mw.ustring.format( li.errMsgs.unknownParams,
			table.concat( unknown, ', ' ) ) )
	end
end

-- Map functions

local function apiLocationMap( args )
	local map = argCheck( args.map, 'missing' )
	local success, mapData = pcall( getMapData, map )
	if not success then
		return mw.ustring.format( li.errMsgs.unknownMap, map )
	end
	
	-- Parameters check
	addMaintenance( checkParameters( args, lp.locationMap ) )
	if not args.lat or not args.long then
		addMaintenance( li.errMsgs.notANumber )
		return getMaintenance()
	end
	args.x, args.y, success = checkCoordinate( args, mapData )

	args.maptype = argCheck( args.maptype, 'default' )
	args.mapImage = argCheck( args.alternativeMap, getMapImage( mapData, args.maptype ) )
	if ( args.mapImage or '' ) == '' then
		success = false
		addMaintenance( li.errMsgs.noMapImage )
	end
	if not success then
		return getMaintenance()
	end

	argsCheck( args, { 'caption', 'captionStyle', 'captionInnerBorder',
		'captionOuterBorder', 'places', 'mapStyle' } )

	-- Image size and description
	args.width = argCheck( tostring( args.width ), '' )
	if not args.width:match( '^%d+$' ) and not args.width:match( '^%d*x%d+$' ) then
		args.width = li.defaults.mapSize
	end
	args.description = mapData.description or ''

	args = checkMarkerProperties( args, mapData )
	return baseMap( args ) .. getMaintenance()
end

local function apiAddLocation( args )
	local map = argCheck( args.map, 'missing' )
	local success, mapData = pcall( getMapData, map )
	if not success then
		return mw.ustring.format( li.errMsgs.unknownMap, map )
	end

	-- Parameters check
	addMaintenance( checkParameters( args, lp.locationMapLocation ) )
	if not args.lat or not args.long then
		addMaintenance( li.errMsgs.notANumber )
		return getMaintenance()
	end
	args.x, args.y, success = checkCoordinate( args, mapData )
	if not success then
		return getMaintenance()
	end

	args = checkMarkerProperties( args, mapData )
	return setLocation( args ) .. getMaintenance()
end

local function apiAddObject( args )
	argsCheck( args, { 'object', 'right', 'left', 'top', 'bottom', 'objectStyle',
		'objectBackground' } )

	if args.object == '' then
		return li.errMsgs.noObject
	end
	
	local success = true
	addMaintenance( checkParameters( args, lp.locationMapObject ) )

	if args.right == '' and args.left == '' then
		success = false
		addMaintenance( li.errMsgs.noXPos )
	end
	if args.top == '' and args.bottom == '' then
		success = false
		addMaintenance( li.errMsgs.noYPos )
	end
	if not success then
		return getMaintenance()
	end

	if args.objectBackground ~='' then
		args.objectBackground = 'background: ' .. args.objectBackground
		if args.objectStyle ~='' then
			args.objectStyle = args.objectStyle .. '; ' .. args.objectBackground
		else
			args.objectStyle = args.objectBackground
		end
	end

	local style, labelClass
	if args.left ~= '' then
		style = 'left: ' .. args.left .. ';'
	else
		style = 'right: ' .. args.right .. ';'
	end
	if args.top ~= '' then
		style = style .. 'top: ' .. args.top .. ';'
	else
		style = style .. 'bottom: ' .. args.bottom .. ';'
	end
	labelClass, args.objectStyle = getClass( args.objectStyle, li.labelClasses )
	style = style .. args.objectStyle
	return tostring( mw.html.create( 'div' )
		:addClass( 'voy-locmap-object' )
		:addClass( labelClass )
		:cssText( isSet( style ) )
		:wikitext( args.object )
	) .. getMaintenance()
end

-- Documentation of map data

local function apiGetMapValue( args )
	local map = argCheck( args.map, 'missing' )
	local success, mapData = pcall( getMapData, map )
	if not success then
		return mw.ustring.format( li.errMsgs.unknownMap, map )
	end
	
	args.param = argCheck( args.param, '' )
	if args.param == '' then
		return li.errMsgs.noParam
	else
		return mapData[ args.param ] or li.errMsgs.anError
	end
end

local function apiGetMapValueSet( args )
	local map = argCheck( args.map, 'missing' )
	local success, mapData = pcall( getMapData, map )
	if not success then
		return mw.ustring.format( li.errMsgs.unknownMap, map )
	end
	
	local row, v
	local list = mw.html.create( 'table' )
		:addClass( li.mapDocs.tableClass )
	for i, j in ipairs( li.paramList ) do
		v = mapData[ j ]
		if not v then
			v = li.errMsgs.notDefined
		else
			if j == 'default' or j == 'relief' then
				v = mw.ustring.format( '[[c:File:%s|%s]]', v, v )
			elseif li.mapDocs[ v ] then
				v = li.mapDocs[ v ]
			end
		end
		row = mw.html.create( 'tr' )
			:node(
				mw.html.create( 'th' )
					:css( 'text-align', 'left' )
					:wikitext( li.mapDocs[ j ] )
			)
			:node(
				mw.html.create( 'td' )
					:wikitext( v )
			)
		list:node( row )
	end
	local titleObj = mw.title.getCurrentTitle()
	if titleObj.text == titleObj.baseText then
		-- not a subpage
		if not mapData.relief then
			addMaintenance( li.errMsgs.noReliefMap )
		end
	end

	return tostring( list ) .. getMaintenance()
end

-- API function calls

local function templateStyles()
	local frame = mw.getCurrentFrame()
	return frame:extensionTag( 'templatestyles', '', { src = 'Module:Location map/styles.css' } );
end

locMap[ li.api.apiLocationMap ] = function( frame )
	return templateStyles() .. apiLocationMap( frame.args )
end

locMap[ li.api.apiAddLocation ] = function( frame )
	return apiAddLocation( frame.args )
end

locMap[ li.api.apiAddObject ] = function( frame )
	return apiAddObject( frame.args )
end

locMap[ li.api.apiGetMapValue ] = function( frame )
	return apiGetMapValue( frame.args )
end

locMap[ li.api.apiGetMapValueSet ] = function( frame )
	return apiGetMapValueSet(frame.args)
end

-- example for usage in a Lua script

function locMap.exampleLuaCall()
	local frame = {}
	frame.args = {
		map   = 'de',
		lat   = 52.51789,
		long  = 13.38873,
		name  = 'Berlin',
		label = '[[Berlin]]',
	}
	return locMap.locationMap( frame )
end

return locMap