177 lines
6.8 KiB
Lua
177 lines
6.8 KiB
Lua
--[[
|
|
Illarion Server
|
|
|
|
This program is free software: you can redistribute it and/or modify it under
|
|
the terms of the GNU Affero General Public License as published by the Free
|
|
Software Foundation, either version 3 of the License, or (at your option) any
|
|
later version.
|
|
|
|
This program is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
|
PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License along
|
|
with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
]]
|
|
-- basic handling for polygonal areas on the map
|
|
|
|
local class = require("base.class")
|
|
|
|
local M = {}
|
|
|
|
--- representation of a line. All positions have z=0.
|
|
-- @param posStruct Start point
|
|
-- @param posStruct End point
|
|
-- @return LineStruct
|
|
M.Line = class(
|
|
function(obj, startPoint, endPoint)
|
|
obj.startPoint = position(startPoint.x, startPoint.y, 0)
|
|
obj.endPoint = position(endPoint.x, endPoint.y, 0)
|
|
end
|
|
)
|
|
|
|
--- representation of a polygon, requires at least 3 points. All positions have z=0.
|
|
-- @field lineList list(LineStruct)
|
|
-- @field min posStruct minimum position, i.e. the upper left most corner of the bounding box
|
|
-- @field max posStruct maximum position, i.e. the lower right most corner of the bounding box
|
|
-- @field zList list(int) list with valid z values for PIP test
|
|
-- @param list(posStruct) List of points, neighbours are connected, aswell as first and last point of the list
|
|
-- @return PolygonStruct
|
|
M.Polygon = class(
|
|
function(obj, positionList, zList)
|
|
if #positionList < 3 then
|
|
debug("A polygon must have at least 3 points")
|
|
return
|
|
end
|
|
obj.lineList = {}
|
|
local s = positionList[1]
|
|
obj.min = position(s.x,s.y,0)
|
|
obj.max = position(s.x,s.y,0)
|
|
table.insert(positionList, positionList[1]) -- add first point, so there is an edge between first and last point
|
|
for i=2,#positionList do
|
|
table.insert(obj.lineList, M.Line(s, positionList[i]))
|
|
s = positionList[i]
|
|
obj.min.x = math.min(obj.min.x, s.x)
|
|
obj.min.y = math.min(obj.min.y, s.y)
|
|
obj.max.x = math.max(obj.max.x, s.x)
|
|
obj.max.y = math.max(obj.max.y, s.y)
|
|
end
|
|
if zList==nil then
|
|
obj.zList = {0}
|
|
else
|
|
obj.zList = zList
|
|
end
|
|
end
|
|
)
|
|
|
|
--- tests intersection of two lines (actually line segments).
|
|
-- @param LineStruct The otherLine for which intersection with current Line is tested
|
|
-- @param boolean True if self is a horizontal ray. Thus if the start/end point of otherLine is on self then true is returned if and only if the rest of otherLine is below self.
|
|
-- @return boolean True if the two lines intersect, false if no intersection or lines are identical
|
|
function M.Line:intersectsLine(otherLine, selfIsRay)
|
|
-- solve for p1, p2 i.e. the fraction on the two lines from the start point to the intersection point
|
|
local denominator = (otherLine.endPoint.y - otherLine.startPoint.y)*(self.endPoint.x - self.startPoint.x) - (otherLine.endPoint.x - otherLine.startPoint.x)*(self.endPoint.y - self.startPoint.y)
|
|
local nominator1 = (otherLine.endPoint.x - otherLine.startPoint.x)*(self.startPoint.y - otherLine.startPoint.y) - (otherLine.endPoint.y - otherLine.startPoint.y)*(self.startPoint.x - otherLine.startPoint.x)
|
|
local nominator2 = (self.endPoint.x - self.startPoint.x)*(self.startPoint.y - otherLine.startPoint.y) - (self.endPoint.y - self.startPoint.y)*(self.startPoint.x - otherLine.startPoint.x)
|
|
-- debug("d=" .. denominator .. "; n1=" .. nominator1 .. "; n2=" .. nominator2);
|
|
if denominator == 0 then
|
|
if nominator1==0 and nominator2==0 then
|
|
return false
|
|
end
|
|
return false
|
|
end
|
|
local p1 = nominator1 / denominator -- fraction of self
|
|
local p2 = nominator2 / denominator -- fraction of otherLine
|
|
if selfIsRay then
|
|
-- intersections with start (0) or end (1) point
|
|
if p2==0 then
|
|
-- start of otherLine is on self, check if end is below
|
|
return (otherLine.endPoint.y < otherLine.startPoint.y)
|
|
elseif p2==1 then
|
|
-- end of otherLine is on self, check if start is below
|
|
return (otherLine.startPoint.y < otherLine.endPoint.y)
|
|
end
|
|
end
|
|
-- debug("p1=" .. p1 .. "; p2=" .. p2);
|
|
-- intersection point is only on both line segments if 0 < p1,p2 < 1
|
|
-- otherwise intersection point is on the line, but not on the segments
|
|
if (0<=p1) and (p1<=1) and (0<=p2) and (p2<=1) then
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
--- tests if a point is on a line.
|
|
-- @param posStruct The point to be tested.
|
|
-- @return boolean True if point is on Line
|
|
function M.Line:pointOnLine(point)
|
|
-- check x coordinate
|
|
local px = -1
|
|
local py = -2
|
|
local xOkay = false
|
|
local yOkay = false
|
|
if self.startPoint.x == self.endPoint.x then
|
|
-- horizontal line
|
|
if point.x ~= self.startPoint.x then
|
|
return false
|
|
end
|
|
xOkay = true
|
|
else
|
|
px = (point.x - self.startPoint.x) / (self.endPoint.x - self.startPoint.x)
|
|
if not ((0<=px) and (px<=1)) then
|
|
return false
|
|
end
|
|
end
|
|
-- check y coordinate
|
|
if self.startPoint.y == self.endPoint.y then
|
|
-- vertical line
|
|
if point.y ~= self.startPoint.y then
|
|
return false
|
|
end
|
|
yOkay = true
|
|
else
|
|
py = (point.y - self.startPoint.y) / (self.endPoint.y - self.startPoint.y)
|
|
if not ((0<=py) and (py<=1)) then
|
|
return false
|
|
end
|
|
end
|
|
-- if the line is horizontal or vertical, then we're alright here.
|
|
-- otherwise the fractions px,py have to be equal as the point has to move linearly on the line
|
|
return (xOkay or yOkay or (px == py))
|
|
end
|
|
|
|
--- Point-In-Polygon test
|
|
-- @param posStruct The point to be tested if inside the Polygon
|
|
-- @return boolean True if point is in Polygon
|
|
function M.Polygon:pip(point)
|
|
-- valid z level?
|
|
local zValid = false
|
|
for _,level in pairs(self.zList) do
|
|
if (point.z == level) then
|
|
zValid = true
|
|
break
|
|
end
|
|
end
|
|
if not zValid then
|
|
return false
|
|
end
|
|
-- coarse test: point in bounding box?
|
|
if not ( self.min.x <= point.x and self.min.y <= point.y and point.x <= self.max.x and point.y <= self.max.y) then
|
|
return false
|
|
end
|
|
-- create a test line from the point to the right most boundary
|
|
local testLine = M.Line(point, position(self.max.x+1, point.y, 0))
|
|
local count = 0
|
|
for _,curLine in pairs(self.lineList) do
|
|
if curLine:pointOnLine(point) then
|
|
return true
|
|
end
|
|
if testLine:intersectsLine(curLine, true) then
|
|
count = count + 1
|
|
end
|
|
end
|
|
return (count%2 == 1)
|
|
end
|
|
|
|
return M
|