モジュール:Location map

require('strict')
local p = {}
local getArgs = require('Module:Arguments').getArgs
local function round(n, decimals)
	local pow = 10^(decimals or 0)
	return math.floor(n * pow + 0.5) / pow
end
function p.getMapParams(map, frame)
	if not map then
		error('Location mapの名前は必ず指定しなければなりません。', 2)
	end
	local moduletitle = mw.title.new('Module:Location map/data/' .. map)
	if not moduletitle then
		error(string.format('%qはLocation mapの名前として無効です。', map), 2)
	elseif moduletitle.exists then
		local mapData = mw.loadData('Module:Location map/data/' .. map)
		return function(name, params)
			if name == nil then
				return 'Module:Location map/data/' .. map
			elseif mapDataname == nil then
				return 
			elseif params then
				return mw.message.newRawMessage(tostring(mapDataname), unpack(params)):plain()
			else
				return mapDataname
			end
		end
	elseif mw.title.new('Template:Location map ' .. map).exists then
		local cache = {}
		if type(frame) ~= 'table' or type(frame.expandTemplate) ~= 'function' then
			error('古いLocation mapを使うときにフレームが存在しなければなりません。')
		end
		return function(name, params)
			if params then
				return frame:expandTemplate{title = 'Location map ' .. map, args = { name, unpack(params) }}
			else
				if name == nil then
					return 'Template:Location map ' .. map
				elseif cachename == nil then
					cachename = frame:expandTemplate{title = 'Location map ' .. map, args = { name }}
				end
				return cachename
			end
		end
	else
		error('Location mapのモジュール「"Module:Location map/data/' .. map .. '"」もしくはテンプレート「"Template:Location map ' .. map .. '"」が作成されていません。', 2)
	end
end
function p.data(frame, args, map)
	if not args then
		args = getArgs(frame, {frameOnly = true})
	end
	if not map then
		map = p.getMapParams(args1, frame)
	end
	local params = {}
	for k,v in ipairs(args) do
		if k > 2 then
			paramsk-2 = v
		end
	end
	return map(args2, #params ~= 0 and params)
end
local hemisphereMultipliers = {
	longitude = { W = -1, w = -1, E = 1, e = 1 },
	latitude = { S = -1, s = -1, N = 1, n = 1 }
}
local function decdeg(degrees, minutes, seconds, hemisphere, decimal, direction)
	if decimal then
		if degrees then
			error(direction .. 'において10進数形式と度分秒は両方指定できません。', 2)
		elseif minutes then
			error(direction .. 'において分は度分秒方式でのみ指定してください。', 2)
		elseif seconds then
			error(direction .. 'において秒は度分秒方式でのみ指定してください。', 2)
		elseif hemisphere then
			error(direction .. 'において半球は度分秒方式でのみ指定してください。', 2)
		end
		local retval = tonumber(decimal)
		if retval then
			return retval
		end
		error(direction .. 'の値"' .. decimal .. '"は無効です。', 2)
	elseif seconds and not minutes then
		error(direction .. 'の秒を指定する時は分も指定してください。', 2)
	elseif not degrees then
		if minutes then
			error(direction .. 'の分を指定する時は度も指定してください。', 2)
		elseif hemisphere then
			error(direction .. 'の半球を指定する時は度も指定してください。', 2)
		end
		return nil
	end
	decimal = tonumber(degrees)
	if not decimal then
		error(direction .. 'の度の値"' .. degrees .. '"は無効です。', 2)
	elseif minutes and not tonumber(minutes) then
		error(direction .. 'の分の値"' .. minutes .. '"は無効です。', 2)
	elseif seconds and not tonumber(seconds) then
		error(direction .. 'の秒の値"' .. seconds .. '"は無効です。', 2)
	end
	decimal = decimal + (minutes or 0)/60 + (seconds or 0)/3600
	if hemisphere then
		local multiplier = hemisphereMultipliersdirectionhemisphere
		if not multiplier then
			error(direction .. 'の半球である"' .. hemisphere .. '"は無効です。', 2)
		end
		decimal = decimal * multiplier
	end
	return decimal
end
-- Finds a parameter in a transclusion of .
local function coord2text(para,coord) -- this should be changed for languages which do not use Arabic numerals or the degree sign
	local i18nmap = {'東' = 'E', '南' = 'S', '西' = 'W', '北' = 'N'}
	local lat, long = mw.ustring.match(coord,'(^(^')
	if lat then
		return tonumber(para == 'longitude' and long or lat)
	end
	local result = mw.text.split(mw.ustring.match(coord,'%-?%.%d+°NS %-?%.%d+°EW') or , ' °')
	if table.maxn(result) == 1 then
		result = mw.text.split(mw.ustring.match(coord,'南北緯%-?%.%d+度 東西経%-?%.%d+度') or , '緯経度 ')
		if table.maxn(result) ~= 1 then
			result = {result2, i18nmapresult1, result5, i18nmapresult4}
		end
	end
	if para == 'longitude' then result = {result3, result4} end
	if not tonumber(result1) or not result2 then
		mw.log('不正な座標指定です。')
		mw.logObject(para, 'para')
		mw.logObject(coord, 'coord')
		return error('不正な座標指定です。', 2)
	end
	return tonumber(result1) * hemisphereMultiplierspararesult2
end
-- effectively make removeBlanks false for caption and maplink, and true for everything else
-- if useWikidata is present but blank, convert it to false instead of nil
-- p.top, p.bottom, and their callers need to use this
function p.valueFunc(key, value)
	if value then
		value = mw.text.trim(value)
	end
	if value ~=  or key == 'caption' or key == 'maplink' then
		return value
	elseif key == 'useWikidata' then
		return false
	end
end
local function getContainerImage(args, map)
	if args.AlternativeMap then
		return args.AlternativeMap
	elseif args.relief and map('image1') ~=  then
		return map('image1')
	else
		return map('image')
	end
end
function p.top(frame, args, map)
	if not args then
		args = getArgs(frame, {frameOnly = true, valueFunc = p.valueFunc})
	end
	if not map then
		map = p.getMapParams(args1, frame)
	end
	local width
	local default_as_number = tonumber(mw.ustring.match(tostring(args.default_width),"%d*"))
	if not args.width then
		width = round((default_as_number or 240) * (tonumber(map('defaultscale')) or 1))
	elseif mw.ustring.sub(args.width, -2) == 'px' then
		width = mw.ustring.sub(args.width, 1, -3)
	else
		width = args.width
	end
	local width_as_number = tonumber(mw.ustring.match(tostring(width),"%d*")) or 0;
if width_as_number == 0 then
	-- check to see if width is junk. If it is, then use default calculation
	width = round((default_as_number or 240) * (tonumber(map('defaultscale')) or 1))
	width_as_number = tonumber(mw.ustring.match(tostring(width),"%d*")) or 0;
end	
if args.max_width ~= "" and args.max_width ~= nil then
-- check to see if width bigger than max_width
local max_as_number = tonumber(mw.ustring.match(args.max_width,"%d*")) or 0;
if width_as_number>max_as_number and max_as_number>0 then
width = args.max_width;
end
end
	local retval = frame:extensionTag{name = 'templatestyles', args = {src = 'Module:Location map/styles.css'}}
	if args.float == 'center' then
		retval = retval .. 
	end
	if args.caption and args.caption ~=  and args.border ~= 'infobox' then
		retval = retval ..  or '">')
	else
		retval = retval .. 
	end
	local image = getContainerImage(args, map)
	local currentTitle = mw.title.getCurrentTitle()
	retval = string.format(
		'%s%s%s',
		retval,
		image,
		width,
		args.alt or ((args.label or currentTitle.text) .. 'の位置（' .. map('name')) .. '内）',
		args.maplink and ('|link=' .. args.maplink) or 
	)
	if args.caption and args.caption ~=  then
		if (currentTitle.namespace == 0) and mw.ustring.find(args.caption, '##') then
			retval = retval .. 
		end
	end
	if args.overlay_image then
		return retval ..  .. width .. 'px'
	else
		return retval
	end
end
function p.bottom(frame, args, map)
	if not args then
		args = getArgs(frame, {frameOnly = true, valueFunc = p.valueFunc})
	end
	if not map then
		map = p.getMapParams(args1, frame)
	end
	local retval = 
	local currentTitle = mw.title.getCurrentTitle()
	if not args.caption or args.border == 'infobox' then
		if args.border then
			retval = retval .. 
		else
			retval = retval .. 
		end
		retval = retval
		.. (args.caption or (args.label or currentTitle.text) .. ' (' .. map('name') .. ')')
		.. 
	elseif args.caption ~=   then
		-- This is not the pipe trick. We're creating a link with no text on purpose, so that CSS can give us a nice image
		retval = retval .. ' ' .. args.caption .. 
	end
	if args.switcherLabel then
		retval = retval ..  .. args.switcherLabel .. 
	elseif args.autoSwitcherLabel then
		retval = retval ..  .. map('name') .. 'の地図を表示'
	end
	
	retval = retval .. 
	if args.caption_undefined then
		mw.log('caption_undefinedは廃止されたパラメータです。')
		local parent = frame:getParent()
		if parent then
			mw.log('Parent is ' .. parent:getTitle())
		end
		mw.logObject(args, 'args')
		if currentTitle.namespace == 0 then
			retval = retval .. 
		end
	end
	if map('skew') ~=  or map('lat_skew') ~=  or map('crosses180') ~=  or map('type') ~=  then
		mw.log(map() .. 'において廃止されたパラメータです。')
		if currentTitle.namespace == 0 then
			local key = (map('skew') ~=  and 'skew' or ) ..
					(map('lat_skew') ~=  and 'lat_skew' or ) ..
					(map('crosses180') ~=  and 'crosses180' or ) ..
					(map('type') ~=  and 'type' or )
			retval = retval .. 
		end
	end
	if string.find(map('name'), '|', 1, true) then
		mw.log(map() .. 'のLocation map名でパイプを使用しています。')
		if currentTitle.namespace == 0 then
			retval = retval .. 
		end
	end
	if args.float == 'center' then
		retval = retval .. 
	end
	return retval
end
local function markOuterDiv(x, y, imageDiv, labelDiv)
	return mw.html.create('div')
		:addClass('od')
		:cssText('top:' .. round(y, 3) .. '%;left:' .. round(x, 3) .. '%')
		:node(imageDiv)
		:node(labelDiv)
end
local function markImageDiv(mark, marksize, label, link, alt, title)
	local builder = mw.html.create('div')
		:addClass('id')
		:cssText('left:-' .. round(marksize / 2) .. 'px;top:-' .. round(marksize / 2) .. 'px')
		:attr('title', title)
	if marksize ~= 0 then
		builder:wikitext(string.format(
			'link=%s%s',
			mark,
			marksize,
			marksize,
			label,
			link,
			alt and ('|alt=' .. alt) or 
		))
	end
	return builder
end
local function markLabelDiv(label, label_size, label_width, position, background, x, marksize)
	if tonumber(label_size) == 0 then
		return mw.html.create('div'):addClass('l0'):wikitext(label)
	end
	local builder = mw.html.create('div')
		:cssText('font-size:' .. label_size .. '%;width:' .. label_width .. 'em')
	local distance = round(marksize / 2 + 1)
	local spanCss
	if position == 'top' then -- specified top
		builder:addClass('pv'):cssText('bottom:' .. distance .. 'px;left:' .. (-label_width / 2) .. 'em')
	elseif position == 'bottom' then -- specified bottom
		builder:addClass('pv'):cssText('top:' .. distance .. 'px;left:' .. (-label_width / 2) .. 'em')
	elseif position == 'left' or (tonumber(x) > 70 and position ~= 'right') then -- specified left or autodetected to left
		builder:addClass('pl'):cssText('right:' .. distance .. 'px')
	else -- specified right or autodetected to right
		builder:addClass('pr'):cssText('left:' .. distance .. 'px')
	end
	builder = builder:tag('div')
		:wikitext(label)
	if background then
		builder:cssText('background-color:' .. background)
	end
	return builder:done()
end
local function getX(longitude, left, right)
	local width = (right - left) % 360
	if width == 0 then
		width = 360
	end
	local distanceFromLeft = (longitude - left) % 360
	-- the distance needed past the map to the right equals distanceFromLeft - width. the distance needed past the map to the left equals 360 - distanceFromLeft. to minimize page stretching, go whichever way is shorter
	if distanceFromLeft - width / 2 >= 180 then
		distanceFromLeft = distanceFromLeft - 360
	end
	return 100 * distanceFromLeft / width
end
local function getY(latitude, top, bottom)
	return 100 * (top - latitude) / (top - bottom)
end
function p.mark(frame, args, map)
	if not args then
		args = getArgs(frame, {wrappers = 'Template:Location map~'})
	end
	local mapnames = {}
	if not map then
		if args1 then
			map = {}
			for mapname in mw.text.gsplit(args1, '#', true) do
				map#map + 1 = p.getMapParams(mw.ustring.gsub(mapname, '^%s*(.-)%s*$', '%1'), frame)
				mapnames#mapnames + 1 = mapname
			end
			if #map == 1 then map = map1 end
		else
			map = p.getMapParams('World', frame)
			args1 = 'World'
		end
	end
	if type(map) == 'table' then
		local outputs = {}
		local oldargs = args1
		for k,v in ipairs(map) do
			args1 = mapnamesk
			outputsk = tostring(p.mark(frame, args, v))
		end
		args1 = oldargs
		return table.concat(outputs, '#PlaceList#') .. '#PlaceList#'
	end
	local x, y, longitude, latitude
	longitude = decdeg(args.lon_deg, args.lon_min, args.lon_sec, args.lon_dir, args.long, 'longitude')
	latitude = decdeg(args.lat_deg, args.lat_min, args.lat_sec, args.lat_dir, args.lat, 'latitude')
	if args.excludefrom then
		-- If this mark is to be excluded from certain maps entirely (useful in the context of multiple maps)
		for exclusionmap in mw.text.gsplit(args.excludefrom, '#', true) do
			-- Check if this map is excluded. If so, return an empty string.
			if args1 == exclusionmap then
				return 
			end
		end
			
	end
	local builder = mw.html.create()
	local currentTitle = mw.title.getCurrentTitle()
	if args.coordinates then
--		Temporarily removed to facilitate infobox conversion.
--		if longitude or latitude then
--			error('Module:Coordinatesからの座標とローカル指定座標の両方を提供できません。')
--		end
		longitude = coord2text('longitude', args.coordinates)
		latitude = coord2text('latitude', args.coordinates)
	elseif not longitude and not latitude and args.useWikidata then
		-- If they didn't provide either coordinate, try Wikidata. If they provided one but not the other, don't.
		local entity = mw.wikibase.getEntity()
		if entity and entity.claims and entity.claims.P625 and entity.claims.P6251.mainsnak.snaktype == 'value' then
			local value = entity.claims.P6251.mainsnak.datavalue.value
			longitude, latitude = value.longitude, value.latitude
		end
		if args.link and (currentTitle.namespace == 0) then
			builder:wikitext() --英語版ではウィキデータから座標を取得したという追跡カテゴリ
		end
	end
	if not longitude then
		error('緯度の値が指定されていません。')
	elseif not latitude then
		error('経度の値が指定されていません。')
	end
	if currentTitle.namespace > 0 then
		if (not args.lon_deg) ~= (not args.lat_deg) then
			builder:wikitext() --英語版では緯度と経度の精度が異なるという追跡カテゴリ
		elseif (not args.lon_min) ~= (not args.lat_min) then
			builder:wikitext() --英語版では緯度と経度の精度が異なるという追跡カテゴリ
		elseif (not args.lon_sec) ~= (not args.lat_sec) then
			builder:wikitext() --英語版では緯度と経度の精度が異なるという追跡カテゴリ
		elseif (not args.lon_dir) ~= (not args.lat_dir) then
			builder:wikitext() --英語版では緯度と経度の精度が異なるという追跡カテゴリ
		elseif (not args.long) ~= (not args.lat) then
			builder:wikitext() --英語版では緯度と経度の精度が異なるという追跡カテゴリ
		end
	end
	if ((tonumber(args.lat_deg) or 0)  100 or y  100) and not args.outside then
		mw.log('外部フラグ設定無しに地図外にマークが置かれました。 x = ' .. x .. ', y = ' .. y)
		local parent = frame:getParent()
		if parent then
			mw.log('親は' .. parent:getTitle() .. 'です。')
		end
		mw.logObject(args, 'args')
		if currentTitle.namespace == 0 then
			local key = currentTitle.prefixedText
			builder:wikitext() --英語版では座標が地図の外かつoutside引数が未指定という追跡カテゴリ
		end
	end
	local mark = args.mark or map('mark')
	if mark ==  then
		mark = 'Red pog.svg'
	end
	local marksize = tonumber(args.marksize) or tonumber(map('marksize')) or 8
	local imageDiv = markImageDiv(mark, marksize, args.label or mw.title.getCurrentTitle().text, args.link or , args.alt, args2)
	local labelDiv
	if args.label and args.position ~= 'none' then
		labelDiv = markLabelDiv(args.label, args.label_size or 91, args.label_width or 6, args.position, args.background, x, marksize)
	end
	return builder:node(markOuterDiv(x, y, imageDiv, labelDiv))
end
local function switcherSeparate(s)
	if s == nil then return {} end
	local retval = {}
	for i in string.gmatch(s .. '#', '(^#*)#') do
		i = mw.text.trim(i)
		retval#retval + 1 = (i ~=  and i)
	end
	return retval
end
function p.main(frame, args, map)
	local caption_list = {}
	if not args then
		args = getArgs(frame, {wrappers = 'Template:Location map', valueFunc = p.valueFunc})
	end
	if args.useWikidata == nil then
		args.useWikidata = true
	end
	if not map then
		if args1 then
			map = {}
			for mapname in string.gmatch(args1, '^#+') do
				map#map + 1 = p.getMapParams(mw.ustring.gsub(mapname, '^%s*(.-)%s*$', '%1'), frame)
			end
			if args'caption' then
				if args'caption' == "" then
					while #caption_list  #map then
			error(string.format('%dのAlternativeMapsが提供されていますが、%dの地図しか指定されていません。', #altmaps, #map))
		end
		local overlays = switcherSeparate(args.overlay_image)
		if #overlays > #map then
			error(string.format('%dのoverlay_imagesが提供されていますが、%dの地図しか指定されていません。', #overlays, #map))
		end
		if #caption_list > #map then
			error(string.format('%dのcaptionが提供されていますが、%dの地図しか指定されていません。', #caption_list, #map))
		end
		local outputs = {}
		args.autoSwitcherLabel = true
		for k,v in ipairs(map) do
			args.AlternativeMap = altmapsk
			args.overlay_image = overlaysk
			args.caption = caption_listk
			outputsk = p.main(frame, args, v)
		end
		return  .. table.concat(outputs) .. 
	else
		return p.top(frame, args, map) .. tostring( p.mark(frame, args, map) ) .. p.bottom(frame, args, map)
	end
end
return p