Module

LinesOverlay

From Dogcraft Wiki

The {{LinesOverlay}} template uses Module:LinesOverlay to draw lines and place icons based on coordinates. This is intended for marking connections and facilities of the transport networks on the Dogcraft Server to be overlayed on a map. This module uses line by line markup for generating the lines and icons.

This template is intended to be used to create an overlay to be overlayed on a map, but can still be used for other purposes if so desired. The lines it creates are not particularly nice at sharp corners, as due to the limitations of Mediawiki lines are drawn with html <div> elements.

Usage

This template can generate a effectively unlimited number of lines and/or icons. It is important to note however, that this template only accepts a single parameter: a block of text of special markup.

Template opener

Open a LinesOverlay template with the template identifier: {{LinesOverlay|

Lines Overlay Markup

The markup is parsed line by line, meaning that every line of text given to the template acts as one parameter.
Please note that spaces ' ' and line breaks '\n' are significant in markup for this template and should only be used where needed. Generally pieces of information are sperated by a single space ' ' in this templates markup and each line of markup acts like a parameter and provides a full set of information for one element. Every line begins with a line identifier and this is followed by information for this identifier.

This template accepts 3 types of information in markup: info, icon and line

Coordinate blocks

The template markup accepts many sets of coordinates, which are all specified as a coordinate block.
A coordinate block consists of a X and a Z coordinate separated by a comma ','.
For example the coordinates X:500, Z:300 translate to the following coordinate block: 500,300


The info line

info is an optional line that can be specified in markup. If used, this line should be the first under the template opener.
The info line can set a coordinate offset and a specific size and thus aspect ratio of the output.
The info line begins with the info identifier, which is followed by 2 coordinate blocks.
The first coordinate block specifies offset coordinates for the output, which sets the top-left corner of the output to the given coordinates. The default for this is 0,0.
The second coordinate block specifies a custom size of the output. The x coordinate in this case specifies width and the z coordinate specifies height. The default for this is 500,500.

Example

info 500,-1000 1000,500
This will set the top-left corner of the output to be at coordinates X:500, Z:-1000 and will set the width of the output to be 1000 and the height to be 500, making the output twice as wide as it is high.


Icons

In addition to drawing lines, you can also place specific icons at specific coordinates with this template.
There are 5 predefined icons and you have the ability to specify a custom FontAwesome icon as well.
The 5 predefined icons are:
station
port
shelter
portal
hub

The predefined icons use the identifier shown above and accept a single coordinate block after the identifier. This coordinate block specifies the coordinates where the icon will be located.

A custom icon can be specified using the icon identifier.
This identifier accepts 1 coordinate block and after that a unspecified number of css class names. This can be used with FontAwesome classes to specify an arbitrary icon.
The coordinate block, similar to the predefined icons, specifies the coordinates at which the icon will be located.
The css class names are inserted into the class attribute of a <i> html tag. If FontAwesome classes are used here, this can result in a FontAwesome icon being displayed.

Examples
Markup Result
station 500,354 Located at coordinates X:500, Z:354
icon 500,354 fa-solid fa-door-open Located at coordinates X:500, Z:354
icon 845,-512 fa-solid fa-sailboat fa-house Located at coordinates X:845, Z:-512 1
icon 845,-512 fa-solid fa-sailboat green Located at coordinates X:845, Z:-512 2
icon 845,-512 fa-solid fa-sailboat otherclass Located at coordinates X:845, Z:-512 2

1 Note that multiple different icons overwrite each other and only one of the icon classes will have effect, thus only showing one icon.
2 Note that the class "otherclass" is not styled in any way on this page and thus has no effect. The class "green" sets the color to green, thus changing the color of the icon.


Lines

Lines use the identifier line and accept first a color and width block and after that an arbitrary number of coordinate blocks.
The color and width block consists of a valid css color and a number for the line width (thickness) separated by a colon ':'.
Specifying the width is optional and defaults to 5.
CSS colors can be a word (eg "green"), a hex color (eg "#ff0000"), a rgb color (eg "rgb(255, 0, 0)"), or a rgba color (eg "rgba(255, 0, 0, 0.5)"). Please check the internet for examples or a list of available color names.

The following coordinate blocks are read left to right and the line is drawn in segments from point to point.
All coordinate blocks on the same line of text will be used to draw a single visible line.
To draw a line splitting to 2 lines, you can either specify them as 2 lines (the starting line continuing after the split as one of the two ends and the other end as a separate line) or as 3 lines (the starting line and 2 separate ends).
Lines can cross, overlap and coincide arbitrarily. Multiple lines can also have points at the exact same coordinates.

Examples
Markup Result
line green 0,0 10,10 10,20 20,40
line red 10,10 30,20 50,40
line #0000ff60 10,10 30,20 40,40 30,40 30,10


Close Template

Dont forget to close the template below the last line of markup with }}!


Full example

Here is an example using most available features:

{{LinesOverlay|
info 0,-500
station 258,-147
shelter 313,-154
port 271,-66
portal 272,-235
icon 323,-353 fa-solid fa-dog
line #ff4500 237,-91 306,-91 324,-95 413,-95 434,-92 478,-72 514,-72 521,-60
line #ff4500 302,-91 305,-82 305,-66 308,-51 308,-25 327,8
line #ff4500 275,-92 274,-121 283,-134 284,-156 282,-168 269,-216 269,-231 274,-250 289,-262 297,-278 399,-379 399,-393 397,-396 385,-396 382,-399 382,-454 396,-468 396,-489 394,-491 387,-491 369,-509
line lime 269,-135 276,-132 328,-126 360,-100 422,-100 436,-97 468,-82 484,-83 489,-85 543,-85
line yellow 248,-139 244,-136 245,-130 253,-126 279,-126 304,-119 312,-111 315,-99 315,-56 318,-47 323,-43 328,-41 353,-41 361,-38 363,-34 365,-29 365,-13 367,-7 373,-2 383,0 389,6
}}



-- Copyright (c) 2024, XPModder, Dogcraft.net and contributors
-- 
-- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"),
-- to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
-- and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
-- 
-- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-- 
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
-- WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-- 
-- End license text.

-- A Module for generating lines and icons based on coordinates of each corner
local p = {}
local data = ""
local startX = 0
local startZ = 0
local width = 500
local height = 500


-- Each line of the input is one drawn line or one Icon on the overlay
-- Parses the line and generates HTML for it
local function parseLine(line)
	
	line = mw.text.trim(line)
	
	local type = ""
	local output = ""
	local continueIcon = 0
	
	local width1Percent = width / 100
	local height1Percent = height / 100
	
	-- Split the line into comma separated key-pairs by spaces
    local pairs = mw.text.split(line, " ", true)
    for i = 1, #pairs do
        -- Get the value
        local value = pairs[i]
        
        -- accepted parameters are line, station, shelter, port, portal and icon
        if (i == 1) then
        	
        	type = mw.text.trim(value)
        	
        else
        	
        	-- type line takes a css color value as second argument and after that an arbitrary amount of coordinates
        	if (type == "line") then
        		
        		local color = "green"
        		
        		if (i == 2) then
        		
        			local vals = mw.text.split(value, ":", true)
        		
        			color = vals[1]
        			local lineWidth = tonumber(vals[2])
        			
        			if (lineWidth == nil) then
        				lineWidth = 5	
        			end
        			
        			lineWidth = lineWidth / width1Percent
        			
        			output = output .. "<div class='outerLinesContainer' style='--line-color: " .. color .. "; --line-width:" .. lineWidth .. "%;'>"
        		
        		else
        			
        			-- lines are drawn in pieces, each piece being drawn by starting at the current coordinates and drawing to the next coordinates
        			local nextValue = pairs[i+1]
        			
        			-- if there is no set of coordinates after this one, we have reached the end of the line and close the outerLinesContainer before returning the content
        			if (nextValue == nil) then
        				
        				output = output .. "</div>"
        				
        				return output
        				
        			end
        			
        			-- get the coordinates
        			local startCoords = mw.text.split(value, ",", true)
        			local endCoords = mw.text.split(nextValue, ",", true)
        			
        			local continue = 0
        			
        			-- start x and z as well as end x and z are smaller then starting point
        			if (tonumber(startCoords[1]) < startX) then
        				if (tonumber(startCoords[2]) < startZ) then
        					if (tonumber(endCoords[1]) < startX) then
        						if (tonumber(endCoords[2]) < startZ) then
        							continue = 1
        						end
        					end
        				end
        			end
        			
        			-- start x and end x are smaller then starting point
        			if (tonumber(startCoords[1]) < startX) then
        				if (tonumber(endCoords[1]) < startX) then
        					continue = 1
        				end
        			end
        			
        			-- start z and end z are smaller then starting point
        			if (tonumber(startCoords[2]) < startZ) then
        				if (tonumber(endCoords[2]) < startZ) then
        					continue = 1
        				end
        			end
        			
        			-- start x and z as well as end x and z are bigger then end point
        			if (tonumber(startCoords[1]) > (startX + width)) then
        				if (tonumber(startCoords[2]) > (startZ + height)) then
        					if (tonumber(endCoords[1]) > (startX + width)) then
        						if (tonumber(endCoords[2]) > (startZ + height)) then
        							continue = 1
        						end
        					end
        				end
        			end
        			
        			-- start x and end x are bigger then ending point
        			if (tonumber(startCoords[1]) > (startX + width)) then
        				if (tonumber(endCoords[1]) > (startX + width)) then
        					continue = 1
        				end
        			end
        			
        			-- start z and end z are bigger then ending point
        			if (tonumber(startCoords[2]) > (startZ + height)) then
        				if (tonumber(endCoords[2]) > (startZ + height)) then
        					continue = 1
        				end
        			end
        			
        			-- if the entire line is outside the visible area, skip it
        			if (continue == 0) then
        			
	        			-- calculate the css values for the line segment
	        			local top = tostring((tonumber(startCoords[2]) - startZ) / height1Percent) .. "%"
	        			local left = tostring((tonumber(startCoords[1]) - startX) / width1Percent) .. "%"
	        			
	        			local xdiff = tonumber(startCoords[1]) - tonumber(endCoords[1])
	        			local zdiff = tonumber(endCoords[2]) - tonumber(startCoords[2])
	        			
	        			-- protect against divide by 0
	        			if (zdiff == 0) then
	        				zdiff = 0.0000001
	        			end
	        			
	        			local angle = math.atan( xdiff / zdiff )
	        			
	        			local transform = "rotate("
	        			
	        			if (zdiff < 0) then
	        				transform = transform .. "calc(" .. tostring(angle) .. "rad + 180deg)"
	        			else
	        				transform = transform .. tostring(angle) .. "rad"
	        			end
	        			
	        			transform = transform .. ")"
	        			
	        			xdiff = tonumber(endCoords[1]) - tonumber(startCoords[1])
	        			
	        			local height = tostring((math.sqrt( (xdiff ^ 2) + (zdiff ^ 2) )) / height1Percent) .. "%"
	        			
	        			
	        			-- assemble all the data into one div for this line segment and append it to the output
	        			output = output .. "<div class='line' style='top: " .. top .. "; left: " .. left .. "; height: " .. height .. "; transform: " .. transform .. ";'></div>"
        			
        			end
        			
        		end
        		
        	-- stations only accept the coordinates of the station as second parameter, nothing else is parsed
        	elseif (type == "station") then
        		
        		if (i == 2) then
        			
        			local coords = mw.text.split(value, ",", true)
        			
        			local continue = 0
        			
        			if (tonumber(coords[1]) < startX) then
        				if (tonumber(coords[2]) < startZ) then
        					continue = 1
        				end
        			end
        			
        			if (tonumber(coords[1]) > (startX + width)) then
        				if (tonumber(coords[2]) > (startZ + height)) then
        					continue = 1
        				end
        			end
        			
        			if (continue == 0) then
        		
	        			local top = tostring((tonumber(coords[2]) - startZ) / height1Percent) .. "%"
	        			local left = tostring((tonumber(coords[1]) - startX) / width1Percent) .. "%"
	        		
	        			output = output .. "<div class='station' style='top: calc(" .. top .. " - 0.5em); left: calc(" .. left .. " - 0.5em);'>"
	        		
	        			output = output .. "<i class='fa-solid fa-train'></i></div>"
	        			
	        			return output
	        			
	        		end
        		
        		end
        		
        	-- ports behave like stations, just with a different icon
        	elseif (type == "port") then
        		
        		if (i == 2) then
        			
        			local coords = mw.text.split(value, ",", true)
        			
        			local continue = 0
        			
        			if (tonumber(coords[1]) < startX) then
        				if (tonumber(coords[2]) < startZ) then
        					continue = 1
        				end
        			end
        			
        			if (tonumber(coords[1]) > (startX + width)) then
        				if (tonumber(coords[2]) > (startZ + height)) then
        					continue = 1
        				end
        			end
        			
        			if (continue == 0) then
        		
	        			local top = tostring((tonumber(coords[2]) - startZ) / height1Percent) .. "%"
	        			local left = tostring((tonumber(coords[1]) - startX) / width1Percent) .. "%"
	        		
	        			output = output .. "<div class='port' style='top: calc(" .. top .. " - 0.5em); left: calc(" .. left .. " - 0.5em);'>"
	        		
	        			output = output .. "<i class='fa-solid fa-anchor'></i></div>"
	        			
	        			return output
	        			
	        		end
        		
        		end
        		
        	-- shelters are again like stations and ports, just with a different icon
        	elseif (type == "shelter") then
        		
        		if (i == 2) then
        			
        			local coords = mw.text.split(value, ",", true)
        			
        			local continue = 0
        			
        			if (tonumber(coords[1]) < startX) then
        				if (tonumber(coords[2]) < startZ) then
        					continue = 1
        				end
        			end
        			
        			if (tonumber(coords[1]) > (startX + width)) then
        				if (tonumber(coords[2]) > (startZ + height)) then
        					continue = 1
        				end
        			end
        			
        			if (continue == 0) then
        		
	        			local top = tostring((tonumber(coords[2]) - startZ) / height1Percent) .. "%"
	        			local left = tostring((tonumber(coords[1]) - startX) / width1Percent) .. "%"
	        		
	        			output = output .. "<div class='shelter' style='top: calc(" .. top .. " - 0.5em); left: calc(" .. left .. " - 0.5em);'>"
	        		
	        			output = output .. "<i class='fa-solid fa-horse'></i></div>"
	        			
	        			return output
	        			
	        		end
        		
        		end
        		
        	-- portals are also like stations, ports and shelters, with a different icon
        	elseif (type == "portal") then
        		
        		if (i == 2) then
        			
        			local coords = mw.text.split(value, ",", true)
        			
        			local continue = 0
        			
        			if (tonumber(coords[1]) < startX) then
        				if (tonumber(coords[2]) < startZ) then
        					continue = 1
        				end
        			end
        			
        			if (tonumber(coords[1]) > (startX + width)) then
        				if (tonumber(coords[2]) > (startZ + height)) then
        					continue = 1
        				end
        			end
        			
        			if (continue == 0) then
        		
	        			local top = tostring((tonumber(coords[2]) - startZ) / height1Percent) .. "%"
	        			local left = tostring((tonumber(coords[1]) - startX) / width1Percent) .. "%"
	        		
	        			output = output .. "<div class='portal' style='top: calc(" .. top .. " - 0.5em); left: calc(" .. left .. " - 0.5em);'>"
	        		
	        			output = output .. "<i class='fa-solid fa-dungeon'></i></div>"
	        			
	        			return output
	        			
	        		end
        		
        		end
        		
        	-- hubs are also like stations, ports and shelters, with a different icon
        	elseif (type == "hub") then
        		
        		if (i == 2) then
        			
        			local coords = mw.text.split(value, ",", true)
        			
        			local continue = 0
        			
        			if (tonumber(coords[1]) < startX) then
        				if (tonumber(coords[2]) < startZ) then
        					continue = 1
        				end
        			end
        			
        			if (tonumber(coords[1]) > (startX + width)) then
        				if (tonumber(coords[2]) > (startZ + height)) then
        					continue = 1
        				end
        			end
        			
        			if (continue == 0) then
        		
	        			local top = tostring((tonumber(coords[2]) - startZ) / height1Percent) .. "%"
	        			local left = tostring((tonumber(coords[1]) - startX) / width1Percent) .. "%"
	        		
	        			output = output .. "<div class='hub' style='top: calc(" .. top .. " - 0.5em); left: calc(" .. left .. " - 0.5em);'>"
	        		
	        			output = output .. "<i class='fa-solid fa-compass'></i></div>"
	        			
	        			return output
	        			
	        		end
        		
        		end
        		
        	-- icon type accepts the coordinates as second parameter and the icon (fa class) as all further parameters
        	elseif (type == "icon") then
        		
        		-- second parameter, get the coords and open the div and i
        		if (i == 2) then
        			
        			local coords = mw.text.split(value, ",", true)
        			
        			local continue = 0
        			continueIcon = 0
        			
        			if (tonumber(coords[1]) < startX) then
        				if (tonumber(coords[2]) < startZ) then
        					continue = 1
        					continueIcon = 1
        				end
        			end
        			
        			if (tonumber(coords[1]) > (startX + width)) then
        				if (tonumber(coords[2]) > (startZ + height)) then
        					continue = 1
        					continueIcon = 1
        				end
        			end
        			
        			if (continue == 0) then
        		
	        			local top = tostring((tonumber(coords[2]) - startZ) / height1Percent) .. "%"
	        			local left = tostring((tonumber(coords[1]) - startX) / width1Percent) .. "%"
	        		
	        			output = output .. "<div class='icon' style='top: calc(" .. top .. " - 0.5em); left: calc(" .. left .. " - 0.5em);'>"
	        		
	        			output = output .. "<i class='" 
	        			
	        		end
        			
        		-- all further parameters after that, add parameter content to class field of i
        		else
        			
        			if (continueIcon == 0) then
        			
	        			output = output .. value .. " "
	        			
	        			-- when we are on the last parameter, close the i and div
	        			if (pairs[i+1] == nil) then
	        				
	        				output = output .. "'></i></div>"
	    			
	    					return output
	        				
	        			end
        			
        			end
        			
        		end
        		
        	-- info type line should be the first line if used
        	-- its second parameter are x and z start coordinates for the entire overlay
        	-- the third parameter is the total width and height (defaults to 500 x 500)
        	-- can potentially be expanded to provide additional info
        	elseif (type == "info") then
        		
        		if (i == 2) then
        			
        			local coords = mw.text.split(value, ",", true)
        			
        			startX = tonumber(coords[1])
        			startZ = tonumber(coords[2])
        			
        			if (startX == nil) then
        				startX = 0	
        			end
        			
        			if (startZ == nil) then
        				startZ = 0
        			end
        		
        		elseif (i == 3) then
        			
        			local sizes = mw.text.split(value, ",", true)
        			
        			width = tonumber(sizes[1])
        			height = tonumber(sizes[2])
        			
        			if (width == nil) then
        				width = 500
        			end
        			
        			if (height == nil) then
        				height = 500
        			end
        			
        		end
        		
        	-- comment line. dont do anything with it
        	elseif (type == "comment") then
        		
        	end
        	
        end

    end

	return ""
	
end



-- Generates the html for provided coordinates
local function generate()
    -- Trim data of whitespace
    data = mw.text.trim(data)

    -- Use mw.text.split to split the data into lines
    local lines = mw.text.split(data, "\n")

    -- Iterate through each line, calling parseLine on it and appending the string result of that to an output data
    local output = ""
    for _, line in ipairs(lines) do
        output = output .. parseLine(line)
    end
    return "<div class='lines-overlay-container' style='max-width: " .. width .. "px; max-height: " .. height .. "px; aspect-ratio: " .. width .. "/" .. height .. ";'>" .. output .. "</div>"
end

-- The main function called on #invoke via the template
function p.linesOverlay(frame)
    local args = {}
    if frame == mw.getCurrentFrame() then
        args = frame:getParent().args
    else
        args = frame
    end

    -- Iterate through each key pair in args
    for key, value in pairs(args) do
        -- Add to data
        data = data .. value
    end

    return generate()
end

return p