Skip to content

Commit ffe4d6e

Browse files
authored
overload hash and equality (#147)
* overload hash and equality * Update src/geos_types.jl * bump version * wip * fix hash * implement == via equals * implement coordinatewise equality * overload Base.isapprox * more isapprox tests
1 parent f56f3f9 commit ffe4d6e

File tree

4 files changed

+169
-1
lines changed

4 files changed

+169
-1
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "LibGEOS"
22
uuid = "a90b1aa1-3769-5649-ba7e-abc5a9d163eb"
33
license = "MIT"
4-
version = "0.7.5"
4+
version = "0.7.6"
55

66
[deps]
77
CEnum = "fa961155-64e5-5f13-b03f-caf6b980ea82"

src/LibGEOS.jl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,13 @@ mutable struct GEOSContext
127127
end
128128
end
129129

130+
function Base.:(==)(c1::GEOSContext, c2::GEOSContext)
131+
(c1.ptr == c2.ptr)
132+
end
133+
function Base.hash(c::GEOSContext, h::UInt)
134+
hash(c.ptr, h)
135+
end
136+
130137
"Get a copy of a string from GEOS, freeing the GEOS managed memory."
131138
function string_copy_free(s::Cstring, context::GEOSContext = get_global_context())::String
132139
copy = unsafe_string(s)

src/geos_types.jl

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,97 @@ const geomtypes = [
309309
GeometryCollection,
310310
]
311311

312+
Base.@kwdef struct IsApprox
313+
atol::Float64=0.0
314+
rtol::Float64=sqrt(eps(Float64))
315+
end
316+
317+
function Base.:(==)(geo1::AbstractGeometry, geo2::AbstractGeometry)::Bool
318+
compare(==, geo1, geo2)
319+
end
320+
function Base.isequal(geo1::AbstractGeometry, geo2::AbstractGeometry)::Bool
321+
compare(isequal, geo1, geo2)
322+
end
323+
function Base.isapprox(geo1::AbstractGeometry, geo2::AbstractGeometry; kw...)::Bool
324+
compare(IsApprox(;kw...), geo1, geo2)
325+
end
326+
function (cmp::IsApprox)(geo1::AbstractGeometry, geo2::AbstractGeometry)::Bool
327+
compare(cmp, geo1, geo2)
328+
end
329+
function compare(, geo1::AbstractGeometry, geo2::AbstractGeometry)::Bool
330+
(typeof(geo1) === typeof(geo2)) || return false
331+
ng1 = ngeom(geo1)
332+
ng2 = ngeom(geo2)
333+
ng1 == ng2 || return false
334+
for i in 1:ng1
335+
(getgeom(geo1, i) getgeom(geo2, i)) || return false
336+
end
337+
return true
338+
end
339+
function compare(, pt1::Point, pt2::Point)::Bool
340+
is3d = GeoInterface.is3d(pt1)
341+
is3d === GeoInterface.is3d(pt2) || return false
342+
GeoInterface.getcoord(pt1,1) GeoInterface.getcoord(pt2,1) || return false
343+
GeoInterface.getcoord(pt1,2) GeoInterface.getcoord(pt2,2) || return false
344+
if is3d
345+
GeoInterface.getcoord(pt1,3) GeoInterface.getcoord(pt2,3) || return false
346+
end
347+
return true
348+
end
349+
350+
function _norm(x,y,z)
351+
return sqrt(x^2 + y^2 + z^2)
352+
end
353+
354+
function compare(cmp::IsApprox, pt1::Point, pt2::Point)::Bool
355+
is3d = GeoInterface.is3d(pt1)
356+
is3d === GeoInterface.is3d(pt2) || return false
357+
x1 = GeoInterface.getcoord(pt1,1)
358+
x2 = GeoInterface.getcoord(pt2,1)
359+
y1 = GeoInterface.getcoord(pt1,2)
360+
y2 = GeoInterface.getcoord(pt2,2)
361+
if is3d
362+
z1 = GeoInterface.getcoord(pt1,3)
363+
z2 = GeoInterface.getcoord(pt2,3)
364+
else
365+
z1 = 0.0
366+
z2 = 0.0
367+
end
368+
lhs = _norm(x1 - x2, y1 - y2, z1 - z2)
369+
rhs = cmp.atol + cmp.rtol * max(_norm(x1,y1,z2), _norm(x2,y2,z2))
370+
return lhs <= rhs
371+
end
372+
373+
typesalt(::Type{GeometryCollection} ) = 0xd1fd7c6403c36e5b
374+
typesalt(::Type{PreparedGeometry} ) = 0xbc1a26fe2f5b7537
375+
typesalt(::Type{LineString} ) = 0x712352fe219fca15
376+
typesalt(::Type{LinearRing} ) = 0xac7644fd36955ef1
377+
typesalt(::Type{MultiLineString} ) = 0x85aff0a53a2f2a32
378+
typesalt(::Type{MultiPoint} ) = 0x6213e67dbfd3b570
379+
typesalt(::Type{MultiPolygon} ) = 0xff2f957b4cdb5832
380+
typesalt(::Type{Point} ) = 0x4b5c101d3843160e
381+
typesalt(::Type{Polygon} ) = 0xa5c895d62ef56723
382+
383+
function Base.hash(geo::AbstractGeometry, h::UInt)::UInt
384+
salt = typesalt(typeof(geo))
385+
h = hash(salt, h)
386+
for i in 1:ngeom(geo)
387+
g = getgeom(geo,i)
388+
h = hash(g, h)
389+
end
390+
return h
391+
end
392+
393+
function Base.hash(pt::Point, h::UInt)::UInt
394+
h = hash(getcoord(pt,1), h)
395+
h = hash(getcoord(pt,2), h)
396+
if GeoInterface.is3d(pt)
397+
h = hash(getcoord(pt,3), h)
398+
end
399+
h = hash(getcoord(pt,2), h)
400+
return h
401+
end
402+
312403
# teach ccall how to get the pointer to pass to libgeos
313404
# this way the Julia compiler will track the lifetimes for us
314405
Base.unsafe_convert(::Type{Ptr{Cvoid}}, x::AbstractGeometry) = x.ptr

test/test_misc.jl

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Test
22
using LibGEOS
3+
import GeoInterface
34

45
@testset "allow_global_context!" begin
56
Point(1,2,3)
@@ -45,6 +46,75 @@ end
4546
@test LibGEOS.intersects(p2, q1, ctx3)
4647
end
4748

49+
@testset "hash eq" begin
50+
ctx1 = LibGEOS.GEOSContext()
51+
ctx2 = LibGEOS.GEOSContext()
52+
@test ctx1 != ctx2
53+
@test ctx1 == ctx1
54+
@test hash(ctx1) != hash(ctx2)
55+
pt1 = readgeom("POINT(0.12345 2.000 0.1)")
56+
pt2 = readgeom("POINT(0.12345 2.000 0.2)")
57+
@test pt1 != pt2
58+
@test hash(pt1) != hash(pt2)
59+
@test !isequal(pt1, pt2)
60+
@test !isapprox(pt1, pt2)
61+
@test !isapprox(pt1, pt2, atol=0, rtol=0)
62+
@test isapprox(pt1, pt2, atol=0.2)
63+
@test isapprox(pt1, pt2, rtol=0.1)
64+
@test readgeom("LINESTRING (130 240, 650 240)") != readgeom("LINESTRING (130 240, -650 240)")
65+
@test !(readgeom("LINESTRING (130 240, 650 240)") readgeom("LINESTRING (130 240, -650 240)"))
66+
@test readgeom("LINESTRING (130 240, 650 240)") readgeom("LINESTRING (130 240, -650 240)") atol=1300
67+
@test readgeom("LINESTRING (130 240, 650 240)") readgeom("LINESTRING (130 240, 650 240.00000001)")
68+
69+
pt = readgeom("POINT(0 NaN)")
70+
@test isequal(pt, pt)
71+
@test pt != pt
72+
@test !(isapprox(pt, pt))
73+
@test !(isapprox(pt, pt, atol=Inf))
74+
75+
geo = readgeom("POLYGON((1 1,1 2,2 2,2 NaN,1 1))")
76+
@test isequal(geo,geo)
77+
@test geo != geo
78+
@test !(isapprox(geo, geo))
79+
@test !(isapprox(geo, geo, atol=Inf))
80+
81+
geos = [
82+
readgeom("POINT(0.12345 2.000 0.1)"),
83+
readgeom("POINT(0.12345 2.000)"),
84+
readgeom("POINT(0.12345 2.000 0.2)"),
85+
readgeom("POLYGON EMPTY"),
86+
readgeom("LINESTRING EMPTY"),
87+
readgeom("MULTIPOLYGON(((0 0,0 10,10 10,10 0,0 0)))"),
88+
readgeom("POLYGON((1 1,1 2,2 2,2 1,1 1))"),
89+
readgeom("POLYGON((1 1,1 2,2 2,2.1 1,1 1))"),
90+
readgeom("LINESTRING (130 240, 650 240)"),
91+
readgeom("MULTILINESTRING ((5 0, 10 0), (0 0, 5 0))"),
92+
readgeom("GEOMETRYCOLLECTION (LINESTRING (1 2, 2 2), LINESTRING (2 1, 1 1), POLYGON ((0.5 1, 1 2, 1 1, 0.5 1)), POLYGON ((9 2, 9.5 1, 2 1, 2 2, 9 2)))"),
93+
readgeom("MULTIPOINT(0 0, 5 0, 10 0)"),
94+
]
95+
for g1 in geos
96+
for g2 in geos
97+
if g1 === g2
98+
@test g1 == g2
99+
@test isequal(g1,g2)
100+
@test hash(g1) == hash(g2)
101+
@test isapprox(g1,g2)
102+
else
103+
@test g1 != g2
104+
@test !isequal(g1,g2)
105+
@test !isapprox(g1,g2)
106+
@test hash(g1) != hash(g2)
107+
end
108+
end
109+
end
110+
for g in geos
111+
@test g == LibGEOS.clone(g)
112+
@test g LibGEOS.clone(g)
113+
@test isequal(g, LibGEOS.clone(g))
114+
@test hash(g) == hash(LibGEOS.clone(g))
115+
end
116+
end
117+
48118
@testset "show it like you build it" begin
49119
for geo in [
50120
readgeom("POINT(0 0)")

0 commit comments

Comments
 (0)