diff --git a/CHANGELOG.md b/CHANGELOG.md index ef3e76f7..48b38218 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- Use `Sequence` instead of `[]XY` for DCEL edges. + ## v0.40.1 2022-11-08 diff --git a/geom/dcel.go b/geom/dcel.go index 44f9dcd3..512550a4 100644 --- a/geom/dcel.go +++ b/geom/dcel.go @@ -24,24 +24,14 @@ func (f *faceRecord) String() string { } type halfEdgeRecord struct { - origin *vertexRecord - twin *halfEdgeRecord - incident *faceRecord // only populated in the overlay - next, prev *halfEdgeRecord - intermediate []XY - edgeLabels [2]label - faceLabels [2]label - extracted bool -} - -// secondXY gives the second (1-indexed) XY in the edge. This is either the -// first intermediate XY, or the origin of the next/twin edge in the case where -// there are no intermediates. -func (e *halfEdgeRecord) secondXY() XY { - if len(e.intermediate) == 0 { - return e.twin.origin.coords - } - return e.intermediate[0] + origin *vertexRecord + twin *halfEdgeRecord + incident *faceRecord // only populated in the overlay + next, prev *halfEdgeRecord + seq Sequence + edgeLabels [2]label + faceLabels [2]label + extracted bool } // String shows the origin and destination of the edge (for debugging @@ -50,7 +40,7 @@ func (e *halfEdgeRecord) String() string { if e == nil { return "nil" } - return fmt.Sprintf("%v->%v->%v", e.origin.coords, e.intermediate, e.twin.origin.coords) + return fmt.Sprintf("%v", sequenceToXYs(e.seq)) } type vertexRecord struct { @@ -143,33 +133,30 @@ func (d *doublyConnectedEdgeList) addMultiPolygon(mp MultiPolygon, operand opera for _, ring := range rings { var newEdges []*halfEdgeRecord - forEachNonInteractingSegment(ring, interactions, func(segment []XY) { - // Construct the internal points slices. - intermediateFwd := segment[1 : len(segment)-1] - intermediateRev := reverseXYs(intermediateFwd) - + forEachNonInteractingSegment(ring, interactions, func(segment Sequence) { // Build the edges (fwd and rev). - vertA := d.vertices[segment[0]] - vertB := d.vertices[segment[len(segment)-1]] + reverseSegment := reverseSequence(segment) + 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 - intermediate: intermediateFwd, - 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, + edgeLabels: newHalfPopulatedLabels(operand, true), + faceLabels: newHalfPopulatedLabels(operand, true), } externalEdge := &halfEdgeRecord{ - origin: vertB, - twin: internalEdge, - incident: nil, // only populated in the overlay - next: nil, // populated later - prev: nil, // populated later - intermediate: intermediateRev, - 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, + edgeLabels: newHalfPopulatedLabels(operand, true), + faceLabels: newHalfPopulatedLabels(operand, false), } internalEdge.twin = externalEdge vertA.incidents = append(vertA.incidents, internalEdge) @@ -241,41 +228,39 @@ func (d *doublyConnectedEdgeList) addMultiLineString(mls MultiLineString, operan // Add edges. for i := 0; i < mls.NumLineStrings(); i++ { seq := mls.LineStringN(i).Coordinates() - forEachNonInteractingSegment(seq, interactions, func(segment []XY) { - startXY := segment[0] - endXY := segment[len(segment)-1] + forEachNonInteractingSegment(seq, interactions, func(segment Sequence) { + reverseSegment := reverseSequence(segment) + startXY := segment.GetXY(0) + endXY := reverseSegment.GetXY(0) - intermediateFwd := segment[1 : len(segment)-1] - intermediateRev := reverseXYs(intermediateFwd) - - if edges.containsStartIntermediateEnd(startXY, intermediateFwd, endXY) { + if edges.containsStartIntermediateEnd(segment) { return } - edges.insertStartIntermediateEnd(startXY, intermediateFwd, endXY) - edges.insertStartIntermediateEnd(endXY, intermediateRev, startXY) + edges.insertStartIntermediateEnd(segment) + edges.insertStartIntermediateEnd(reverseSegment) vOrigin := d.vertices[startXY] 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 - intermediate: intermediateFwd, - 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, + edgeLabels: newHalfPopulatedLabels(operand, true), + faceLabels: newUnpopulatedLabels(), } rev := &halfEdgeRecord{ - origin: vDestin, - twin: fwd, - incident: nil, // only populated in overlay - next: fwd, - prev: fwd, - intermediate: intermediateRev, - edgeLabels: newHalfPopulatedLabels(operand, true), - faceLabels: newUnpopulatedLabels(), + origin: vDestin, + twin: fwd, + incident: nil, // only populated in overlay + next: fwd, + prev: fwd, + seq: reverseSegment, + edgeLabels: newHalfPopulatedLabels(operand, true), + faceLabels: newUnpopulatedLabels(), } fwd.twin = rev fwd.next = rev @@ -326,11 +311,10 @@ func (d *doublyConnectedEdgeList) addGhosts(mls MultiLineString, operand operand for i := 0; i < mls.NumLineStrings(); i++ { seq := mls.LineStringN(i).Coordinates() - forEachNonInteractingSegment(seq, interactions, func(segment []XY) { - startXY := segment[0] - endXY := segment[len(segment)-1] - intermediateFwd := segment[1 : len(segment)-1] - intermediateRev := reverseXYs(intermediateFwd) + forEachNonInteractingSegment(seq, interactions, func(segment Sequence) { + reverseSegment := reverseSequence(segment) + startXY := segment.GetXY(0) + endXY := reverseSegment.GetXY(0) if _, ok := d.vertices[startXY]; !ok { d.vertices[startXY] = &vertexRecord{coords: startXY, incidents: nil, labels: [2]label{}} @@ -339,41 +323,41 @@ func (d *doublyConnectedEdgeList) addGhosts(mls MultiLineString, operand operand d.vertices[endXY] = &vertexRecord{coords: endXY, incidents: nil, labels: [2]label{}} } - if edges.containsStartIntermediateEnd(startXY, intermediateFwd, endXY) { + if edges.containsStartIntermediateEnd(segment) { // Already exists, so shouldn't add. return } - edges.insertStartIntermediateEnd(startXY, intermediateFwd, endXY) - edges.insertStartIntermediateEnd(endXY, intermediateRev, startXY) + edges.insertStartIntermediateEnd(segment) + edges.insertStartIntermediateEnd(reverseSegment) - d.addGhostLine(startXY, intermediateFwd, intermediateRev, endXY, operand) + d.addGhostLine(segment, reverseSegment, operand) }) } } -func (d *doublyConnectedEdgeList) addGhostLine(startXY XY, intermediateFwd, intermediateRev []XY, endXY XY, operand operand) { - vertA := d.vertices[startXY] - vertB := d.vertices[endXY] +func (d *doublyConnectedEdgeList) addGhostLine(segment, reverseSegment Sequence, operand operand) { + vertA := d.vertices[segment.GetXY(0)] + 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 - intermediate: intermediateFwd, - 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, + edgeLabels: newHalfPopulatedLabels(operand, false), + faceLabels: [2]label{}, } e2 := &halfEdgeRecord{ - origin: vertB, - twin: e1, - incident: nil, // only populated in the overlay - next: e1, - prev: e1, - intermediate: intermediateRev, - edgeLabels: newHalfPopulatedLabels(operand, false), - faceLabels: [2]label{}, + origin: vertB, + twin: e1, + incident: nil, // only populated in the overlay + next: e1, + prev: e1, + seq: reverseSegment, + edgeLabels: newHalfPopulatedLabels(operand, false), + faceLabels: [2]label{}, } e1.twin = e2 e1.next = e2 @@ -388,7 +372,7 @@ func (d *doublyConnectedEdgeList) addGhostLine(startXY XY, intermediateFwd, inte d.fixVertex(vertB) } -func forEachNonInteractingSegment(seq Sequence, interactions map[XY]struct{}, fn func([]XY)) { +func forEachNonInteractingSegment(seq Sequence, interactions map[XY]struct{}, fn func(Sequence)) { n := seq.Length() i := 0 for i < n-1 { @@ -403,13 +387,8 @@ func forEachNonInteractingSegment(seq Sequence, interactions map[XY]struct{}, fn } } - // Construct the segment. - segment := make([]XY, end-start+1) - for j := range segment { - segment[j] = seq.GetXY(start + j) - } - // Execute the callback with the segment. + segment := seq.Slice(start, end+1) fn(segment) // On the next iteration, start the next edge at the end of diff --git a/geom/dcel_edge_set.go b/geom/dcel_edge_set.go index 2b840afc..c6785511 100644 --- a/geom/dcel_edge_set.go +++ b/geom/dcel_edge_set.go @@ -3,37 +3,32 @@ package geom // edgeSet represents a set of edges in a DCEL. It makes use of assumptions // around proper noding in conjunction with interaction points. // -// Implementation detail: the map key is 3 XYs. The first is the start point of -// the edge, the second is the first intermediate point (or a repeat of the -// first XY if there are no intermediate points), and the third is the start -// point of the next edge. -type edgeSet map[[3]XY]*halfEdgeRecord +// Implementation detail: the map key is 2 XYs. The first is the start point of +// the edge, the second is the second point of the edge (which may or may not +// be the final point of the edge). +type edgeSet map[[2]XY]*halfEdgeRecord -func (s edgeSet) containsStartIntermediateEnd(start XY, intermediate []XY, end XY) bool { - _, ok := s[s.key(start, intermediate, end)] +func (s edgeSet) containsStartIntermediateEnd(segment Sequence) bool { + _, ok := s[s.key(segment)] return ok } func (s edgeSet) insertEdge(e *halfEdgeRecord) { - k := s.key(e.origin.coords, e.intermediate, e.next.origin.coords) + k := s.key(e.seq) s[k] = e } -func (s edgeSet) insertStartIntermediateEnd(start XY, intermediate []XY, end XY) { - k := s.key(start, intermediate, end) +func (s edgeSet) insertStartIntermediateEnd(segment Sequence) { + k := s.key(segment) s[k] = nil // TODO: this is a bit weird... } func (s edgeSet) lookupEdge(e *halfEdgeRecord) (*halfEdgeRecord, bool) { - k := s.key(e.origin.coords, e.intermediate, e.next.origin.coords) + k := s.key(e.seq) e, ok := s[k] return e, ok } -func (edgeSet) key(start XY, intermediate []XY, end XY) [3]XY { - key := [3]XY{start, start, end} - if len(intermediate) > 0 { - key[1] = intermediate[0] - } - return key +func (edgeSet) key(segment Sequence) [2]XY { + return [2]XY{segment.GetXY(0), segment.GetXY(1)} } diff --git a/geom/dcel_extract.go b/geom/dcel_extract.go index 879b59be..69179bde 100644 --- a/geom/dcel_extract.go +++ b/geom/dcel_extract.go @@ -112,9 +112,8 @@ func extractPolygonBoundary(faceSet map[*faceRecord]bool, start *halfEdgeRecord, e := start for { seen[e] = true - xy := e.origin.coords - coords = append(coords, xy.X, xy.Y) - for _, xy := range e.intermediate { + for i := 0; i < e.seq.Length()-1; i++ { + xy := e.seq.GetXY(i) coords = append(coords, xy.X, xy.Y) } @@ -187,18 +186,7 @@ func (d *doublyConnectedEdgeList) extractLineStrings(include func([2]label) bool e.origin.extracted = true e.twin.origin.extracted = true - coords := make([]float64, 4+2*len(e.intermediate)) - coords[0] = e.origin.coords.X - coords[1] = e.origin.coords.Y - for i, xy := range e.intermediate { - coords[2+2*i] = xy.X - coords[3+2*i] = xy.Y - } - coords[len(coords)-2] = e.twin.origin.coords.X - coords[len(coords)-1] = e.twin.origin.coords.Y - - seq := NewSequence(coords, DimXY) - ls, err := NewLineString(seq) + ls, err := NewLineString(e.seq) if err != nil { return nil, err } diff --git a/geom/dcel_overlay.go b/geom/dcel_overlay.go index aadb9995..0aecc5e0 100644 --- a/geom/dcel_overlay.go +++ b/geom/dcel_overlay.go @@ -92,8 +92,8 @@ func (d *doublyConnectedEdgeList) fixVertex(v *vertexRecord) { sort.Slice(v.incidents, func(i, j int) bool { ei := v.incidents[i] ej := v.incidents[j] - di := ei.secondXY().Sub(ei.origin.coords) - dj := ej.secondXY().Sub(ej.origin.coords) + di := ei.seq.GetXY(1).Sub(ei.seq.GetXY(0)) + dj := ej.seq.GetXY(1).Sub(ej.seq.GetXY(0)) aI := math.Atan2(di.Y, di.X) aJ := math.Atan2(dj.Y, dj.X) return aI < aJ diff --git a/geom/dcel_test.go b/geom/dcel_test.go index 1ca99f79..c92b9391 100644 --- a/geom/dcel_test.go +++ b/geom/dcel_test.go @@ -143,7 +143,7 @@ func CheckDCEL(t *testing.T, dcel *doublyConnectedEdgeList, spec DCELSpec) { if seq[len(seq)-1] != e.next.origin.coords { continue } - if !xysEqual(e.intermediate, seq[1:len(seq)-1]) { + if !xysEqual(sequenceToXYs(e.seq), seq) { continue } want = spec.Edges[i] @@ -181,7 +181,7 @@ func xysEqual(a, b []XY) bool { func findEdge(t *testing.T, dcel *doublyConnectedEdgeList, first, second XY) *halfEdgeRecord { for _, e := range dcel.halfEdges { - if e.origin.coords == first && e.secondXY() == second { + if e.seq.GetXY(0) == first && e.seq.GetXY(1) == second { return e } } @@ -202,8 +202,7 @@ func CheckCycle(t *testing.T, f *faceRecord, start *halfEdgeRecord, want []XY) { if e.origin == nil { t.Errorf("edge origin not set") } - got = append(got, e.origin.coords) - got = append(got, e.intermediate...) + got = append(got, sequenceToXYs(e.seq)[:e.seq.Length()-1]...) e = e.next if e == start { got = append(got, e.origin.coords) @@ -227,10 +226,7 @@ func CheckCycle(t *testing.T, f *faceRecord, start *halfEdgeRecord, want []XY) { t.Fatal("inf loop") } - got = append(got, e.next.origin.coords) - for j := len(e.intermediate) - 1; j >= 0; j-- { - got = append(got, e.intermediate[j]) - } + got = append(got, sequenceToXYs(reverseSequence(e.seq))[:e.seq.Length()-1]...) e = e.prev if e == start { diff --git a/geom/util.go b/geom/util.go index 1dc4e5d9..1c8ca82f 100644 --- a/geom/util.go +++ b/geom/util.go @@ -58,12 +58,23 @@ func sortAndUniquifyXYs(xys []XY) []XY { return xys[:n] } -func reverseXYs(fwd []XY) []XY { - rev := make([]XY, len(fwd)) - for i := range rev { - rev[i] = fwd[len(fwd)-1-i] +func reverseSequence(seq Sequence) Sequence { + n := seq.Length() + coords := make([]float64, 0, 2*n) + for i := n - 1; i >= 0; i-- { + xy := seq.GetXY(i) + coords = append(coords, xy.X, xy.Y) } - return rev + return NewSequence(coords, DimXY) +} + +func sequenceToXYs(seq Sequence) []XY { + n := seq.Length() + xys := make([]XY, seq.Length()) + for i := 0; i < n; i++ { + xys[i] = seq.GetXY(i) + } + return xys } // fastMin is a faster but not functionally identical version of math.Min.