Skip to content

Commit

Permalink
Simplify DCEL label management
Browse files Browse the repository at this point in the history
In the DCEL data structure, there are labels on the vertex records,
(half) edge records, and face records. These labels keep track
information such as which input geometry the record originates from and
whether it will appear in the output geometry.

The original approach to labelling was to use the same data structure
for all record types. The label had two booleans per operand.  The first
boolean determined whether the record would appear in the final output,
and the second boolean would show whether the first boolean was
populated or not. This was essentially tri-state logic. The trouble with
this approach was that the DCEL algorithm doesn't know whether each
record should appear in the output geometry right up until the last
stage. The labels are used in the earlier stages of the algorithm to
record other facts, such as whether or not the records appear in the
input geometry. The semantics of the fields subtly change over the
course of the algorithm, making the code hard to understand and
maintain.

The new approach (implemented by this PR) changes how label management
is performed. Instead of having a single labels data structure, the
fields are slightly different for vertex, edge, and face records. This
is to account for the differences in information available when
processing vertexes vs. edges vs. faces. The number of fields is also
expanded, so that concepts like "is this record explicitly present in
the input geometry's control points" are separate from concepts like "is
this record present in the input geometry but not explicitly in its
control points". The populated fields are also removed, and tracked
separately in each algorithm that modifies the labels.
  • Loading branch information
peterstace committed Nov 8, 2022
1 parent c9889c2 commit 45ccc4b
Show file tree
Hide file tree
Showing 7 changed files with 815 additions and 782 deletions.
15 changes: 10 additions & 5 deletions geom/alg_set_op.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ func Union(a, b Geometry) (Geometry, error) {
if b.IsEmpty() {
return a, nil
}
g, err := setOp(a, b, selectUnion)
g, err := setOp(a, or, b)
return g, wrap(err, "executing union")
}

Expand All @@ -24,7 +24,7 @@ func Intersection(a, b Geometry) (Geometry, error) {
if a.IsEmpty() || b.IsEmpty() {
return Geometry{}, nil
}
g, err := setOp(a, b, selectIntersection)
g, err := setOp(a, and, b)
return g, wrap(err, "executing intersection")
}

Expand All @@ -38,7 +38,7 @@ func Difference(a, b Geometry) (Geometry, error) {
if b.IsEmpty() {
return a, nil
}
g, err := setOp(a, b, selectDifference)
g, err := setOp(a, andNot, b)
return g, wrap(err, "executing difference")
}

Expand All @@ -55,11 +55,11 @@ func SymmetricDifference(a, b Geometry) (Geometry, error) {
if b.IsEmpty() {
return a, nil
}
g, err := setOp(a, b, selectSymmetricDifference)
g, err := setOp(a, xor, b)
return g, wrap(err, "executing symmetric difference")
}

func setOp(a, b Geometry, include func([2]label) bool) (Geometry, error) {
func setOp(a Geometry, include func([2]bool) bool, b Geometry) (Geometry, error) {
overlay, err := createOverlay(a, b)
if err != nil {
return Geometry{}, wrap(err, "internal error creating overlay")
Expand All @@ -71,3 +71,8 @@ func setOp(a, b Geometry, include func([2]label) bool) (Geometry, error) {
}
return g, nil
}

func or(b [2]bool) bool { return b[0] || b[1] }
func and(b [2]bool) bool { return b[0] && b[1] }
func xor(b [2]bool) bool { return b[0] != b[1] }
func andNot(b [2]bool) bool { return b[0] && !b[1] }
144 changes: 79 additions & 65 deletions geom/dcel.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@ type doublyConnectedEdgeList struct {
}

type faceRecord struct {
cycle *halfEdgeRecord
labels [2]label
cycle *halfEdgeRecord

// inSet encodes whether this face is part of the input geometry for each
// operand.
inSet [2]bool

extracted bool
}

Expand All @@ -29,9 +33,20 @@ type halfEdgeRecord struct {
incident *faceRecord // only populated in the overlay
next, prev *halfEdgeRecord
seq Sequence
edgeLabels [2]label
faceLabels [2]label
extracted bool

// srcEdge encodes whether or not this edge is explicitly appears as part
// of the input geometries.
srcEdge [2]bool

// srcFace encodes whether or not this edge explicitly borders onto a face
// in the input geometries.
srcFace [2]bool

// inSet encodes whether or not this edge is (explicitly or implicitly)
// part of the input geometry for each operand.
inSet [2]bool

extracted bool
}

// String shows the origin and destination of the edge (for debugging
Expand All @@ -46,7 +61,15 @@ func (e *halfEdgeRecord) String() string {
type vertexRecord struct {
coords XY
incidents []*halfEdgeRecord
labels [2]label

// src encodes whether on not this vertex explicitly appears in the input
// geometries.
src [2]bool

// inSet encodes whether or not this vertex is part of each input geometry
// (although it might not be explicitly encoded there).
inSet [2]bool

locations [2]location
extracted bool
}
Expand Down Expand Up @@ -123,9 +146,9 @@ func (d *doublyConnectedEdgeList) addMultiPolygon(mp MultiPolygon, operand opera
vr := &vertexRecord{
coords: xy,
incidents: nil, // populated later
labels: newHalfPopulatedLabels(operand, true),
locations: newLocationsOnBoundary(operand),
}
vr.src[operand] = true
d.vertices[xy] = vr
}
}
Expand All @@ -139,25 +162,24 @@ func (d *doublyConnectedEdgeList) addMultiPolygon(mp MultiPolygon, operand opera
vertA := d.vertices[segment.GetXY(0)]
vertB := d.vertices[reverseSegment.GetXY(0)]
internalEdge := &halfEdgeRecord{
origin: vertA,
twin: nil, // populated later
incident: nil, // only populated in the overlay
next: nil, // populated later
prev: nil, // populated later
seq: segment,
edgeLabels: newHalfPopulatedLabels(operand, true),
faceLabels: newHalfPopulatedLabels(operand, true),
origin: vertA,
twin: nil, // populated later
incident: nil, // only populated in the overlay
next: nil, // populated later
prev: nil, // populated later
seq: segment,
}
externalEdge := &halfEdgeRecord{
origin: vertB,
twin: internalEdge,
incident: nil, // only populated in the overlay
next: nil, // populated later
prev: nil, // populated later
seq: reverseSegment,
edgeLabels: newHalfPopulatedLabels(operand, true),
faceLabels: newHalfPopulatedLabels(operand, false),
origin: vertB,
twin: internalEdge,
incident: nil, // only populated in the overlay
next: nil, // populated later
prev: nil, // populated later
seq: reverseSegment,
}
internalEdge.srcEdge[operand] = true
internalEdge.srcFace[operand] = true
externalEdge.srcEdge[operand] = true
internalEdge.twin = externalEdge
vertA.incidents = append(vertA.incidents, internalEdge)
vertB.incidents = append(vertB.incidents, externalEdge)
Expand Down Expand Up @@ -197,13 +219,12 @@ func (d *doublyConnectedEdgeList) addMultiLineString(mls MultiLineString, operan
} else {
locs[operand].interior = true
}
d.vertices[xy] = &vertexRecord{
xy,
nil, // populated later
newHalfPopulatedLabels(operand, true),
locs,
false,
vr := &vertexRecord{
coords: xy,
locations: locs,
}
vr.src[operand] = true
d.vertices[xy] = vr
} else {
if onBoundary {
if v.locations[operand].boundary {
Expand Down Expand Up @@ -243,25 +264,23 @@ func (d *doublyConnectedEdgeList) addMultiLineString(mls MultiLineString, operan
vDestin := d.vertices[endXY]

fwd := &halfEdgeRecord{
origin: vOrigin,
twin: nil, // set later
incident: nil, // only populated in overlay
next: nil, // set later
prev: nil, // set later
seq: segment,
edgeLabels: newHalfPopulatedLabels(operand, true),
faceLabels: newUnpopulatedLabels(),
origin: vOrigin,
twin: nil, // set later
incident: nil, // only populated in overlay
next: nil, // set later
prev: nil, // set later
seq: segment,
}
rev := &halfEdgeRecord{
origin: vDestin,
twin: fwd,
incident: nil, // only populated in overlay
next: fwd,
prev: fwd,
seq: reverseSegment,
edgeLabels: newHalfPopulatedLabels(operand, true),
faceLabels: newUnpopulatedLabels(),
origin: vDestin,
twin: fwd,
incident: nil, // only populated in overlay
next: fwd,
prev: fwd,
seq: reverseSegment,
}
fwd.srcEdge[operand] = true
rev.srcEdge[operand] = true
fwd.twin = rev
fwd.next = rev
fwd.prev = rev
Expand All @@ -286,12 +305,11 @@ func (d *doublyConnectedEdgeList) addMultiPoint(mp MultiPoint, operand operand)
record = &vertexRecord{
coords: xy,
incidents: nil,
labels: [2]label{}, // set below
locations: [2]location{}, // set below
}
d.vertices[xy] = record
}
record.labels[operand] = label{populated: true, inSet: true}
record.src[operand] = true
record.locations[operand].interior = true
}
}
Expand All @@ -317,10 +335,10 @@ func (d *doublyConnectedEdgeList) addGhosts(mls MultiLineString, operand operand
endXY := reverseSegment.GetXY(0)

if _, ok := d.vertices[startXY]; !ok {
d.vertices[startXY] = &vertexRecord{coords: startXY, incidents: nil, labels: [2]label{}}
d.vertices[startXY] = &vertexRecord{coords: startXY}
}
if _, ok := d.vertices[endXY]; !ok {
d.vertices[endXY] = &vertexRecord{coords: endXY, incidents: nil, labels: [2]label{}}
d.vertices[endXY] = &vertexRecord{coords: endXY}
}

if edges.containsStartIntermediateEnd(segment) {
Expand All @@ -340,24 +358,20 @@ func (d *doublyConnectedEdgeList) addGhostLine(segment, reverseSegment Sequence,
vertB := d.vertices[reverseSegment.GetXY(0)]

e1 := &halfEdgeRecord{
origin: vertA,
twin: nil, // populated later
incident: nil, // only populated in the overlay
next: nil, // popluated later
prev: nil, // populated later
seq: segment,
edgeLabels: newHalfPopulatedLabels(operand, false),
faceLabels: [2]label{},
origin: vertA,
twin: nil, // populated later
incident: nil, // only populated in the overlay
next: nil, // popluated later
prev: nil, // populated later
seq: segment,
}
e2 := &halfEdgeRecord{
origin: vertB,
twin: e1,
incident: nil, // only populated in the overlay
next: e1,
prev: e1,
seq: reverseSegment,
edgeLabels: newHalfPopulatedLabels(operand, false),
faceLabels: [2]label{},
origin: vertB,
twin: e1,
incident: nil, // only populated in the overlay
next: e1,
prev: e1,
seq: reverseSegment,
}
e1.twin = e2
e1.next = e2
Expand Down
26 changes: 13 additions & 13 deletions geom/dcel_extract.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package geom
import "fmt"

// extractGeometry converts the DECL into a Geometry that represents it.
func (d *doublyConnectedEdgeList) extractGeometry(include func([2]label) bool) (Geometry, error) {
func (d *doublyConnectedEdgeList) extractGeometry(include func([2]bool) bool) (Geometry, error) {
areals, err := d.extractPolygons(include)
if err != nil {
return Geometry{}, err
Expand Down Expand Up @@ -52,12 +52,12 @@ func (d *doublyConnectedEdgeList) extractGeometry(include func([2]label) bool) (
}
}

func (d *doublyConnectedEdgeList) extractPolygons(include func([2]label) bool) ([]Polygon, error) {
func (d *doublyConnectedEdgeList) extractPolygons(include func([2]bool) bool) ([]Polygon, error) {
var polys []Polygon
for _, face := range d.faces {
// Skip any faces not selected to be include in the output geometry, or
// any faces already extracted.
if !include(face.labels) || face.extracted {
if !include(face.inSet) || face.extracted {
continue
}

Expand All @@ -81,7 +81,7 @@ func (d *doublyConnectedEdgeList) extractPolygons(include func([2]label) bool) (
if seen[edge] {
return
}
if include(edge.twin.incident.labels) {
if include(edge.twin.incident.inSet) {
// Adjacent face is in the polygon, so this edge cannot be part
// of the boundary.
seen[edge] = true
Expand Down Expand Up @@ -135,7 +135,7 @@ func extractPolygonBoundary(faceSet map[*faceRecord]bool, start *halfEdgeRecord,

// findFacesMakingPolygon finds all faces that belong to the polygon that
// contains the start face (according to the given inclusion criteria).
func findFacesMakingPolygon(include func([2]label) bool, start *faceRecord) map[*faceRecord]bool {
func findFacesMakingPolygon(include func([2]bool) bool, start *faceRecord) map[*faceRecord]bool {
expanded := make(map[*faceRecord]bool)
toExpand := make(map[*faceRecord]bool)
toExpand[start] = true
Expand All @@ -151,7 +151,7 @@ func findFacesMakingPolygon(include func([2]label) bool, start *faceRecord) map[
adj := adjacentFaces(popped)
expanded[popped] = true
for _, f := range adj {
if !include(f.labels) {
if !include(f.inSet) {
continue
}
if expanded[f] {
Expand All @@ -177,7 +177,7 @@ func orderCCWRingFirst(rings []LineString) {
}
}

func (d *doublyConnectedEdgeList) extractLineStrings(include func([2]label) bool) ([]LineString, error) {
func (d *doublyConnectedEdgeList) extractLineStrings(include func([2]bool) bool) ([]LineString, error) {
var lss []LineString
for _, e := range d.halfEdges {
if shouldExtractLine(e, include) {
Expand All @@ -196,20 +196,20 @@ func (d *doublyConnectedEdgeList) extractLineStrings(include func([2]label) bool
return lss, nil
}

func shouldExtractLine(e *halfEdgeRecord, include func([2]label) bool) bool {
func shouldExtractLine(e *halfEdgeRecord, include func([2]bool) bool) bool {
return !e.extracted &&
include(e.edgeLabels) &&
!include(e.incident.labels) &&
!include(e.twin.incident.labels)
include(e.inSet) &&
!include(e.incident.inSet) &&
!include(e.twin.incident.inSet)
}

// extractPoints extracts any vertices in the DCEL that should be part of the
// output geometry, but aren't yet represented as part of any previously
// extracted geometries.
func (d *doublyConnectedEdgeList) extractPoints(include func([2]label) bool) ([]Point, error) {
func (d *doublyConnectedEdgeList) extractPoints(include func([2]bool) bool) ([]Point, error) {
var pts []Point
for _, vert := range d.vertices {
if include(vert.labels) && !vert.extracted {
if include(vert.inSet) && !vert.extracted {
vert.extracted = true
pt, err := vert.coords.AsPoint()
if err != nil {
Expand Down

0 comments on commit 45ccc4b

Please sign in to comment.