Dokumentaci tohoto modulu lze vytvořit na stránce Modul:Mapframe/Dokumentace

-- inserting a mapframe map
-- This edition was made for the special needs of Wikivoyage. For a use at a
-- Wikipedia please use https://en.wikipedia.org/wiki/Module:Mapframe
-- kopie z Německých wikicest: https://de.wikivoyage.org/w/index.php?title=Modul:Mapframe&oldid=1594994
-- copy from dewikivoyage: https://de.wikivoyage.org/w/index.php?title=Modul:Mapframe&oldid=1594994

-- documentation
local Mapframe = {
	suite  = 'Mapframe',
	serial = '2024-01-26',
	item   = 52554979
}

-- module import
-- require( 'strict' )
local cd = require( 'Module:Coordinates2' )
local mg = mw.loadData( 'Module:Marker utilities/Groups' )
local mi = require( 'Module:Mapshape utilities/i18n' )
local mp = require( 'Module:Mapframe/Params' )
local mu = require( 'Module:Mapshape utilities' )
local wu = require( 'Module:Wikidata utilities' )
local yn = require( 'Module:Yesno' )

-- module variable and administration
local mf = {
	content  = {},
	entityId = nil,
	wikilang = nil,
	moduleInterface = Mapframe
}

-- return decimal coordinate if possible
local function toDec( coord, dir )
	if mu.isSet( coord ) then
		local t = cd.toDec( coord, dir, 6 )
		if t.error == 0 then
			return t.dec
		end
	end
	return nil
end

-- split coordinates to latitude and longitude
local function _parseCoords( coords )
	local lat = nil
	local long = nil
	if not mu.isSet( coords ) then
		return nil, nil
	end

	coords = coords:upper()
	local count
	if not coords:find( '[,]' ) and coords:find( '[NS]' ) then
		coords, count = coords:gsub( '([NS])', '%1,' ) -- adding separator
	end
	local parts = mw.text.split( coords, ',', true )
	if #parts == 2 or #parts == 3 then -- 3: including elevation
		lat = mw.text.trim( parts[ 1 ] )
		long = mw.text.trim( parts[ 2 ] )
		-- check for mask borders
		if lat:find( '36000', 1, true ) and long:find( '180', 1, true ) then
			lat = tonumber( lat )
			long = tonumber( long )
		else
			lat = toDec( lat, 'lat' )
			long = toDec( long, 'long' )
		end
		if not lat or not long then
			return nil, nil
		end
	end
	return lat, long
end

-- parse set of coordinates
-- programmer of function: user Yurik (Yuri Astrakhan), see Modul:Map
local function parseCoords( geoType, coords )
	local geoTypes = {
		Point           = { levels = 1, min = 1 },
		MultiPoint      = { levels = 1, min = 2 },
		LineString      = { levels = 1, min = 2 },
		MultiLineString = { levels = 2, min = 2 },
		Polygon         = { levels = 2, min = 4 },
		MultiPolygon    = { levels = 3, min = 4 }
	}
	local levels = geoTypes[ geoType ].levels
	local min = geoTypes[ geoType ].min

	local results = {}
	for i = 1, levels, 1 do
		results[ i ] = {}
	end
	local gap = 0
	local errors = ''

	local closeArrays = function( gap )
		if #results[ levels ] < min then
			errors = errors .. mw.ustring.format( mi.mfMinValues, min ) .. ' '
		elseif min == 1 and #results[ levels ] ~= 1 then -- Point
			errors = errors .. mi.mfExactlyOne .. ' '
        end
        for i = levels, levels-gap+1, -1 do
            table.insert( results[ i-1 ], results[ i ] )
            results[ i ] = {}
        end
        return 0 -- reset gap
    end

	local points = mw.text.split( coords, ';', true )
	local lat, long, val
	for i = 1, #points, 1 do
		val = mw.text.trim( points[ i ] )
		if val == '' then
			gap = gap + 1
			if gap >= levels then
				errors = errors .. mi.mfTooManyLevels .. ' '
			end
		else
			lat, long = _parseCoords( val )
			if lat and long then
				if gap > 0 then
					gap = closeArrays( gap )
				end
				table.insert( results[ levels ], { long, lat } )
			else
				errors = errors .. mi.mfBadData .. ' '
			end
		end
	end
	closeArrays( levels - 1 )
	if errors == '' then
		return geoType == 'Point' and results[ 1 ][ 1 ] or results[ 1 ], errors
	else
		return nil, errors
	end
end

-- get individual coordinate from Wikidata
local function getWdCoords( id )
	if mu.isSet( id ) then
		local c = wu.getValue( id, 'P625' )
		if c ~= '' then
			return c.latitude, c.longitude
		end
	end
	return nil, nil
end

-- preparing title and description for geoJSON object
local function getTitle( title, description, image, firstId, ids, service )
	local function geomaskTitle()
		if service == 'geomask' then
			title = mw.ustring.format( mi.geomask, title )
		end
	end
	-- getting title if only one id
	if title == '' then
		title = nil
	end
	if mu.isSet( firstId ) and firstId == ids then
		title = mu.addLink( title or mu.getTitle( firstId ), firstId,
			mf.entityId, mf.wikiLang )
		geomaskTitle()
		if not mu.isSet( description ) and not mu.isSet( image ) then
			image = mu.getImage( firstId )
		end
	else
		title = title or mw.title.getCurrentTitle().subpageText
		geomaskTitle()
	end
	if not mu.isSet( description ) and mu.isSet( image ) then
		description = '[[File:' .. image .. '|100x100px]]'
	end
	return title, description
end

-- check for mapshapes Wikidata ids
local function idMatch( only, exclude, id )
	only = only or ''
	exclude = exclude or ''
	if only == '' and exclude == '' then
		return true
 	end

	local function isIn( list )
	 	local parts = mw.text.split( list, ',', true )
		for i = 1, #parts, 1 do
			if mw.text.trim( id ) == mw.text.trim( parts[ i ] ) then
				return true
			end
		end
		return false
	end

 	if only ~= '' then
 		return isIn( only )
	else
		return not isIn( exclude )
	end
end

-- make GeoJSON object for tag call
local function makeGeoJSON( args, argIndex )
	local service = ''
	for key, value in pairs( mp.services ) do
		for key2, value2 in ipairs( value ) do
			if args.service == value2 then
				service = key
				break
			end
		end
		if service ~= '' then
			break
		end
	end
	if service == '' then
		return mi.mfNoService
	end

	local world = '36000,-180;36000,180;-36000,180;-36000,-180;36000,-180;;'
	local coordinates, description, errors, firstId, geojson, geoType, i, id
	local ids, image, lat, link, long, properties, rgb, stroke, title, values

	-- default title and description
	if service ~= 'page' then
		title = args.title
		description = args.description
		image = args.image
	end

	-- processing coordinate data instead of Wikidata IDs
	args.coord = mw.ustring.gsub( args.coord or '', ';*$', '' )
	if mu.isSet( args.coord ) and service ~= 'page' and service ~= 'shapes' then
		if mu.isSet( args.wikidata ) then
			return mi.mfTogether
		end
		if service == 'point' then
			args.coord = mw.ustring.gsub( args.coord, ';;*', ';' )
			if mw.ustring.find( args.coord, ';', 1, true ) then
				geoType = 'MultiPoint'
			else
				geoType = 'Point'
			end
		elseif service == 'geoline' then
			args.coord = mw.ustring.gsub( args.coord, ';;;*', ';;' )
			if mw.ustring.find( args.coord, ';;', 1, true ) then
				geoType = 'MultiLineString'
			else
				geoType = 'LineString'
			end
		elseif service == 'geoshape' or service == 'geomask' then
			if service == 'geomask' then
				args.coord = world .. args.coord
			end
			args.coord = mw.ustring.gsub( args.coord, ';;;;*', ';;;' )
			if mw.ustring.find( args.coord, ';;', 1, true ) then
				geoType = 'MultiPolygon'
			else
				geoType = 'Polygon'
			end
		end
		coordinates, errors = parseCoords( geoType, args.coord )
		if not coordinates then
			return errors
		end

		title, description =
			getTitle( title, description, image, nil, nil, service )

		if geoType == 'Point' or geoType == 'MultiPoint' then
			properties = {
				title = title,
				description = description,
				[ 'marker-symbol' ] = mu.getParameter( args.marker, nil ),
				[ 'marker-color' ] = mu.getParameter( args.markerColor, mi.defaultMarkerColor )
			}
		else
			stroke = args.stroke or ''
			if stroke == '' then
				stroke = mi.defaultStroke
			end
			properties = {
				title = title,
				description = description,
				fill = mu.getParameter( args.fill, mi.defaultFill ),
				[ 'fill-opacity' ] = mu.getNumber( args.fillOpacity, mi.defaultFillOpacity ),
				stroke = stroke,
				[ 'stroke-width' ] = mu.getNumber( args.strokeWidth, mi.defaultStrokeWidth ),
				[ 'stroke-opacity' ] = mu.getNumber( args.strokeOpacity, mi.defaultStrokeOpacity )
			}
		end
		geojson = {
			type = 'Feature',
			geometry = {
				type = geoType,
				coordinates = coordinates
			},
			properties = properties
		}		
		table.insert( mf.content, mw.text.jsonEncode( geojson ) )
		return '' -- no errors
	end
	
	-- processing Wikidata and Wikimedia Commons data
	if service == 'shapes' and not mi.excludeOSM then	
		-- in case of shapes multiple GeoJSON objects are returned
		if not mu.isSet( args.wikidata ) then
			return mi.mfNoWikidata
		end

		values = mu.getMapshapes( args.wikidata )
		if #values == 0 then
			return mi.mfNoParts
		end

		args.defaultType = mu.getParameter( args.defaultType, 'geoline' )
		stroke = args.stroke or ''
		if stroke == '' then
			stroke = args.defaultColor or ''
		end
		if stroke == '' then
			stroke = mi.defaultStroke
		end
		if not string.find( stroke, '#', 1, true ) then
			stroke = '#' .. stroke
		end
				
		for i = 1, #values, 1 do
			id = values[ i ].id
			if idMatch( args.only, args.exclude, id ) then
				title = mu.addLink( mw.wikibase.label( id ) or id, id,
					mf.entityId, mf.wikiLang )
				description = mu.getImage( id )
				if description == '' then
					description = nil
--				else
--					description = '[[File:' .. description .. '|141px]]'
				end

				rgb = mu.getColor( id )
				if rgb == '' then
					rgb = stroke
				end

				geojson = {
					type = 'ExternalData',
					service = args.defaultType,
					ids = id,
					properties = {
						title = title,
						description = description,
						fill = mu.getParameter( args.fill, mi.defaultFill ),
						[ 'fill-opacity' ] = mu.getNumber( args.fillOpacity, mi.defaultFillOpacity ),
						stroke = rgb,
						[ 'stroke-width' ] = mu.getNumber( args.strokeWidth, mi.defaultShapesWidth ),
						[ 'stroke-opacity' ] = mu.getNumber( args.strokeOpacity, mi.defaultShapesOpacity )
					}
				}
				-- collecting multiple geojson codes
				table.insert( mf.content, mw.text.jsonEncode( geojson ) )
			end
		end
		return '' -- objects already inserted, no errors

	elseif service == 'page' then -- data from Wikimedia Commons
		if args.commons then
			geojson = {
				type = 'ExternalData',
				service = 'page',
				title = mw.ustring.gsub( args.commons, '[Dd]ata:', '' )
			}
		else
			return mi.mfNoCommons
		end

	elseif service == 'point' then
		ids = mw.text.split( args.wikidata, ',', true )
		coordinates = {}
		for i = 1, #ids, 1 do
			id = mw.text.trim( ids[ i ] )
			if id ~= '' and mw.wikibase.isValidEntityId( id ) then
				lat, long = getWdCoords( id )
				if lat and long then
					table.insert( coordinates, { long, lat } )
					if #coordinates == 1 then
						title, description =
							getTitle( title, description, image, id, id, service )
					end
				end
			end
		end
		if #coordinates == 0 then
			return mi.mfNoWdCoord
		end

		i = #coordinates == 1
		geojson = {
			type = 'Feature',
			geometry = {
				type = i and 'Point' or 'MultiPoint',
				coordinates = i and coordinates[ 1 ] or coordinates
			},
			properties = {
				title = title,
				description = description,
				[ 'marker-symbol' ] = mu.getParameter( args.marker, nil ),
				[ 'marker-color' ] = mu.getParameter( args.markerColor, mi.defaultMarkerColor )
			}
		}

	-- geoline or geoshape/geomask
	elseif not mi.excludeOSM then
		if mu.isSet( args.wikidata ) then
			ids = args.wikidata
		else
			ids = mf.entityId
		end
		if not mu.isSet( ids ) then
			return mi.mfNoWikidata
		end

		-- getting first id
		firstId = mu.getFirstId( ids )

		title, description =
			getTitle( title, description, image, firstId, ids, service )
				
		-- getting color from first id
		stroke = args.stroke or ''
		if stroke == '' then
			stroke = mu.getColor( firstId )
			if stroke == '' then
				stroke = mi.defaultStroke
			end
		end
		if service == 'geoshape' and argIndex > 0 then
			args.fill = mu.getParameter( args.fill, mi.defaultColors[ argIndex ] )
		end

		geojson = {
			type = 'ExternalData',
			service = service,
			ids = ids,
			properties = {
				title = title,
				description = description,
				fill = mu.getParameter( args.fill, mi.defaultFill ),
				[ 'fill-opacity' ] = mu.getNumber( args.fillOpacity, mi.defaultFillOpacity ),
				stroke = stroke,
				[ 'stroke-width' ] = mu.getNumber( args.strokeWidth, mi.defaultStrokeWidth ),
				[ 'stroke-opacity' ] = mu.getNumber( args.strokeOpacity, mi.defaultStrokeOpacity )
			}
		}
	end
	
	table.insert( mf.content, mw.text.jsonEncode( geojson ) )
	return ''
end

-- processing multiple shape definitions
local function makeTagContent( args )
	local errors = ''
	local r = mu.getParameter( args.raw, nil )
	if r then
		return r, false, errors
	end

	local err = false
	local commons, service, single, wikidata

	local function mergeArgs( indx )
		service = args[ 'type' .. indx ] or ''
		commons = args[ 'page' .. indx ] or ''
		if commons ~= '' then
			service = 'page'
		end
		wikidata = args[ 'wikidata' .. indx ] or ''
		if service == '' and wikidata ~= '' then
			service = 'geomask'
		end
	end

	-- mapgroup parameters
	if args.groupWikidata ~= '' then
		single = {
			service = 'geomask',
			wikidata = args.groupWikidata,
			fill = args.fillMask
		}
		errors = errors .. makeGeoJSON( single, 0 )
	end
	if args.highlightWikidata ~= '' then
		single = {
			service = 'geoshape',
			wikidata = args.highlightWikidata,
			fill = mu.getParameter( args.fill, mi.defaultHighlight )
		}
		errors = errors .. makeGeoJSON( single, 0 )
	end
	
	local argsIndex = ''
	mergeArgs( argsIndex )
	while service ~= '' do
		-- remove index from args parameters and copy them to single
		single = {
			commons = commons,
			wikidata = wikidata,
			service = service
		}
		if commons ~= '' and wikidata ~= '' then
			err = true
		end
		for k, v in pairs( args ) do
			if v == '' then
				v = nil
			end
			if string.match( k, '^[%a%-]+' .. argsIndex .. '$' ) then
				single[ string.gsub( k, argsIndex, '' ) ] = v
			end
		end

		if argsIndex == '' then
			argsIndex = 1
		end
		errors = errors .. makeGeoJSON( single, argsIndex )

		argsIndex = argsIndex + 1
		mergeArgs( argsIndex )
		-- stop if there is no service anymore
	end
	
	if #mf.content == 0 then
		return nil, err, errors
	elseif #mf.content == 1 then
		return mf.content[ 1 ], err, errors
	else
		return '[' .. table.concat( mf.content, ',') .. ']', err, errors
	end
end

-- calling mapframe/maplink tag; addings shapes
local function _mapframe( args, frame )
	local tagArgs = {}
	mf.entityId = mw.wikibase.getEntityIdForCurrentPage()
	mf.wikiLang = mw.getContentLanguage():getCode()

	-- auto-center if tagArgs.latitude = nil or tagArgs.longitude = nil
	tagArgs.latitude = toDec( args.lat, 'lat' )
	tagArgs.longitude = toDec( args.long, 'long' )
	if not tagArgs.latitude or not tagArgs.longitude then
		if args.coords ~= '' then
			tagArgs.latitude, tagArgs.longitude = _parseCoords( args.coords )
		else
			tagArgs.latitude = nil
			tagArgs.longitude = nil
		end
	end
	tagArgs.zoom = tonumber( args.zoom )
	-- auto-zoom if tagArgs.zoom = nil
	if tagArgs.zoom then
		tagArgs.zoom = math.floor( tagArgs.zoom )
		if tagArgs.zoom < 1 or tagArgs.zoom > mi.maxZoomLevel then
			tagArgs.zoom = nil
		end
	end
	if tagArgs.latitude and tagArgs.longitude and not tagArgs.zoom then
		tagArgs.zoom = mi.defaultZoom
	end
	if args.tagName == 'mapframe' then
		tagArgs.align = mu.getParameter( args.align, 'right' )
		if args.width == 'full' then
			tagArgs.width = 'full'
			tagArgs.align = 'center'
		else
			tagArgs.width = mu.getSize( args.width, mi.defaultWidth )
				+ mi.borderAdjustment -- 2px: inside borders
		end
		tagArgs.height = mu.getSize( args.height, mi.defaultHeight )
	end
	tagArgs.show = args.show
	if tagArgs.show ~= '' then
		if args.group ~= '' then
			tagArgs.group = args.group
		else
			if not tagArgs.show:find( ',' ) then
				tagArgs.group = tagArgs.show
			else
				tagArgs.group = mi.defaultGroup
			end
		end
	else
		tagArgs.show = mg.showAll
		tagArgs.group = mu.checkGroup( args.group )
	end
	tagArgs.group = mu.translateGroup( tagArgs.group )
	if not mw.ustring.find( tagArgs.show, tagArgs.group ) then
		tagArgs.show = tagArgs.show .. ',' .. tagArgs.group
	end
	if yn( args.plain, false ) then
		tagArgs.frameless = '1'
	else
		tagArgs.text = args.name
		if tagArgs.text == '' and args.tagName == 'mapframe' then
			tagArgs.text = string.format( mi.mapOf, mw.title.getCurrentTitle().subpageText )
		end
	end
	tagArgs.class = args.class
	if args.tagName == 'maplink' then
		if tagArgs.class == '' and ( tagArgs.text == '' or tagArgs.text == '""' ) then
			-- Hide pushpin icon in front of an empty text link
			tagArgs.class = 'no-icon'
    	end
	end

	local tagContent, err, errors = makeTagContent( args )

	local result = frame:extensionTag( args.tagName, tagContent, tagArgs )
	if err then
		result = result .. mi.mfTogether2
	end
	if mu.isSet( errors ) then
		result = result .. '<span class="error">' .. errors .. '</span>'
			.. mi.mfErrorCateg
	end
	-- adding maintenance categories
	if mw.title.getCurrentTitle().namespace == 0 then
		if mi.usePropertyCategs then
			result = result .. wu.getCategories( mi.properties )
				.. mu.getCategories( mi.properties )
		end
		if tagContent then
			result = result .. mi.mfWithShapes
		end
	end
	return result
end

-- for Mapframe template
function mf.mapframe( frame )
	local args, errors =
		mu.checkParams( frame:getParent().args, mp.params, 'Mapframe', mi.mfUnknown )
	args.tagName = 'mapframe'
	return _mapframe( args, frame ) .. errors
end

-- for Maplink template
function mf.maplink( frame )
	local args, errors =
		mu.checkParams( frame:getParent().args, mp.params, 'Mapframe', mi.mfUnknown )
	local isMapframe = yn( args.frame, false ) -- wp compatibility
	if isMapframe then
		args.tagName = 'mapframe'
	else
		args.tagName = 'maplink'
	end
	return _mapframe( args, frame ) .. errors
end

return mf