2018-10-11 00:25:40 +03:00
|
|
|
// Copyright 2018 Joshua J Baker. All rights reserved.
|
|
|
|
// Use of this source code is governed by an MIT-style
|
|
|
|
// license that can be found in the LICENSE file.
|
|
|
|
|
|
|
|
package geometry
|
|
|
|
|
2018-10-22 05:08:56 +03:00
|
|
|
import (
|
|
|
|
"encoding/binary"
|
|
|
|
"reflect"
|
|
|
|
"unsafe"
|
|
|
|
)
|
2018-10-11 00:25:40 +03:00
|
|
|
|
2018-10-22 05:08:56 +03:00
|
|
|
// IndexKind is the kind of index to use in the options.
|
|
|
|
type IndexKind byte
|
|
|
|
|
|
|
|
// IndexKind types
|
|
|
|
const (
|
|
|
|
None IndexKind = iota
|
|
|
|
RTree
|
|
|
|
RTreeCompressed
|
|
|
|
QuadTree
|
|
|
|
QuadTreeCompressed
|
|
|
|
)
|
|
|
|
|
|
|
|
func (kind IndexKind) String() string {
|
|
|
|
switch kind {
|
|
|
|
default:
|
|
|
|
return "Unknown"
|
|
|
|
case None:
|
|
|
|
return "None"
|
|
|
|
case RTree:
|
|
|
|
return "RTree"
|
|
|
|
case RTreeCompressed:
|
|
|
|
return "RTreeCompressed"
|
|
|
|
case QuadTree:
|
|
|
|
return "QuadTree"
|
|
|
|
case QuadTreeCompressed:
|
|
|
|
return "QuadTreeCompressed"
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// IndexOptions are segment indexing options
|
|
|
|
type IndexOptions struct {
|
|
|
|
Kind IndexKind
|
|
|
|
MinPoints int
|
|
|
|
}
|
|
|
|
|
|
|
|
// DefaultIndexOptions ...
|
|
|
|
var DefaultIndexOptions = &IndexOptions{
|
|
|
|
Kind: QuadTree,
|
|
|
|
MinPoints: 64,
|
|
|
|
}
|
2018-10-11 00:25:40 +03:00
|
|
|
|
|
|
|
// Series is just a series of points with utilities for efficiently accessing
|
|
|
|
// segments from rectangle queries, making stuff like point-in-polygon lookups
|
|
|
|
// very quick.
|
|
|
|
type Series interface {
|
|
|
|
Rect() Rect
|
|
|
|
Empty() bool
|
|
|
|
Convex() bool
|
|
|
|
Clockwise() bool
|
|
|
|
NumPoints() int
|
|
|
|
NumSegments() int
|
|
|
|
PointAt(index int) Point
|
|
|
|
SegmentAt(index int) Segment
|
|
|
|
Search(rect Rect, iter func(seg Segment, index int) bool)
|
2018-10-16 18:55:26 +03:00
|
|
|
Index() interface{}
|
2018-10-11 00:25:40 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func seriesCopyPoints(series Series) []Point {
|
|
|
|
points := make([]Point, series.NumPoints())
|
|
|
|
for i := 0; i < len(points); i++ {
|
|
|
|
points[i] = series.PointAt(i)
|
|
|
|
}
|
|
|
|
return points
|
|
|
|
}
|
|
|
|
|
|
|
|
// baseSeries is a concrete type containing all that is needed to make a Series.
|
|
|
|
type baseSeries struct {
|
|
|
|
closed bool // points create a closed shape
|
|
|
|
clockwise bool // points move clockwise
|
|
|
|
convex bool // points create a convex shape
|
2018-10-22 05:08:56 +03:00
|
|
|
indexKind IndexKind // index kind
|
|
|
|
index interface{} // actual index
|
2018-10-11 00:25:40 +03:00
|
|
|
rect Rect // minumum bounding rectangle
|
|
|
|
points []Point // original points
|
|
|
|
}
|
|
|
|
|
|
|
|
// makeSeries returns a processed baseSeries.
|
2018-10-22 05:08:56 +03:00
|
|
|
func makeSeries(
|
|
|
|
points []Point, copyPoints, closed bool, opts *IndexOptions,
|
|
|
|
) baseSeries {
|
|
|
|
if opts == nil {
|
|
|
|
opts = DefaultIndexOptions
|
|
|
|
}
|
2018-10-11 00:25:40 +03:00
|
|
|
var series baseSeries
|
|
|
|
series.closed = closed
|
|
|
|
if copyPoints {
|
|
|
|
series.points = make([]Point, len(points))
|
|
|
|
copy(series.points, points)
|
|
|
|
} else {
|
|
|
|
series.points = points
|
|
|
|
}
|
2018-10-22 05:08:56 +03:00
|
|
|
series.convex, series.rect, series.clockwise = processPoints(points, closed)
|
|
|
|
if opts.MinPoints != 0 && len(points) >= opts.MinPoints {
|
|
|
|
series.indexKind = opts.Kind
|
|
|
|
series.buildIndex()
|
2018-10-11 00:25:40 +03:00
|
|
|
}
|
|
|
|
return series
|
|
|
|
}
|
|
|
|
|
2018-10-16 18:55:26 +03:00
|
|
|
// Index ...
|
|
|
|
func (series *baseSeries) Index() interface{} {
|
2018-10-22 05:08:56 +03:00
|
|
|
return series.index
|
2018-10-16 18:55:26 +03:00
|
|
|
}
|
|
|
|
|
2018-10-11 00:25:40 +03:00
|
|
|
// Clockwise ...
|
|
|
|
func (series *baseSeries) Clockwise() bool {
|
|
|
|
return series.clockwise
|
|
|
|
}
|
|
|
|
|
|
|
|
func (series *baseSeries) Move(deltaX, deltaY float64) Series {
|
|
|
|
points := make([]Point, len(series.points))
|
|
|
|
for i := 0; i < len(series.points); i++ {
|
|
|
|
points[i].X = series.points[i].X + deltaX
|
|
|
|
points[i].Y = series.points[i].Y + deltaY
|
|
|
|
}
|
2018-10-22 05:08:56 +03:00
|
|
|
nseries := makeSeries(points, false, series.closed, nil)
|
|
|
|
nseries.indexKind = series.indexKind
|
|
|
|
if series.Index() != nil {
|
|
|
|
nseries.buildIndex()
|
2018-10-11 00:25:40 +03:00
|
|
|
}
|
|
|
|
return &nseries
|
|
|
|
}
|
|
|
|
|
|
|
|
// Empty returns true if the series does not take up space.
|
|
|
|
func (series *baseSeries) Empty() bool {
|
|
|
|
if series == nil {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return (series.closed && len(series.points) < 3) || len(series.points) < 2
|
|
|
|
}
|
|
|
|
|
|
|
|
// Rect returns the series rectangle
|
|
|
|
func (series *baseSeries) Rect() Rect {
|
|
|
|
return series.rect
|
|
|
|
}
|
|
|
|
|
|
|
|
// Convex returns true if the points create a convex loop or linestring
|
|
|
|
func (series *baseSeries) Convex() bool {
|
|
|
|
return series.convex
|
|
|
|
}
|
|
|
|
|
|
|
|
// Closed return true if the shape is closed
|
|
|
|
func (series *baseSeries) Closed() bool {
|
|
|
|
return series.closed
|
|
|
|
}
|
|
|
|
|
|
|
|
// NumPoints returns the number of points in the series
|
|
|
|
func (series *baseSeries) NumPoints() int {
|
|
|
|
return len(series.points)
|
|
|
|
}
|
|
|
|
|
|
|
|
// PointAt returns the point at index
|
|
|
|
func (series *baseSeries) PointAt(index int) Point {
|
|
|
|
return series.points[index]
|
|
|
|
}
|
|
|
|
|
|
|
|
// Search finds a searches for segments that intersect the provided rectangle
|
2018-10-22 05:08:56 +03:00
|
|
|
func (series *baseSeries) Search(
|
|
|
|
rect Rect, iter func(seg Segment, idx int) bool,
|
|
|
|
) {
|
|
|
|
switch v := series.index.(type) {
|
|
|
|
default:
|
2018-10-11 00:25:40 +03:00
|
|
|
n := series.NumSegments()
|
|
|
|
for i := 0; i < n; i++ {
|
|
|
|
seg := series.SegmentAt(i)
|
|
|
|
if seg.Rect().IntersectsRect(rect) {
|
|
|
|
if !iter(seg, i) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-10-22 05:08:56 +03:00
|
|
|
case *rTree:
|
|
|
|
v.Search(
|
2018-10-11 00:25:40 +03:00
|
|
|
[]float64{rect.Min.X, rect.Min.Y},
|
|
|
|
[]float64{rect.Max.X, rect.Max.Y},
|
|
|
|
func(_, _ []float64, value interface{}) bool {
|
|
|
|
index := value.(int)
|
|
|
|
var seg Segment
|
|
|
|
seg.A = series.points[index]
|
|
|
|
if series.closed && index == len(series.points)-1 {
|
|
|
|
seg.B = series.points[0]
|
|
|
|
} else {
|
|
|
|
seg.B = series.points[index+1]
|
|
|
|
}
|
|
|
|
if !iter(seg, index) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
},
|
|
|
|
)
|
2018-10-22 05:08:56 +03:00
|
|
|
case *qNode:
|
|
|
|
v.search(series, series.rect, rect, iter)
|
|
|
|
case *byte:
|
|
|
|
// convert the byte pointer back to a valid slice
|
|
|
|
data := *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{
|
|
|
|
Data: uintptr(unsafe.Pointer(v)),
|
|
|
|
Len: 0xFFFFFFFF,
|
|
|
|
Cap: 0xFFFFFFFF,
|
|
|
|
}))
|
|
|
|
n := binary.LittleEndian.Uint32(data[1:])
|
|
|
|
data = data[:n:n]
|
|
|
|
switch data[0] {
|
|
|
|
case 1:
|
|
|
|
rCompressSearch(data, 5, series, rect, iter)
|
|
|
|
case 2:
|
|
|
|
qCompressSearch(data, 5, series, series.rect, rect, iter)
|
|
|
|
}
|
|
|
|
|
2018-10-11 00:25:40 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// NumSegments ...
|
|
|
|
func (series *baseSeries) NumSegments() int {
|
|
|
|
if series.closed {
|
|
|
|
if len(series.points) < 3 {
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
if series.points[len(series.points)-1] == series.points[0] {
|
|
|
|
return len(series.points) - 1
|
|
|
|
}
|
|
|
|
return len(series.points)
|
|
|
|
}
|
|
|
|
if len(series.points) < 2 {
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
return len(series.points) - 1
|
|
|
|
}
|
|
|
|
|
|
|
|
// SegmentAt ...
|
|
|
|
func (series *baseSeries) SegmentAt(index int) Segment {
|
|
|
|
var seg Segment
|
|
|
|
seg.A = series.points[index]
|
|
|
|
if index == len(series.points)-1 {
|
|
|
|
seg.B = series.points[0]
|
|
|
|
} else {
|
|
|
|
seg.B = series.points[index+1]
|
|
|
|
}
|
|
|
|
return seg
|
|
|
|
}
|
|
|
|
|
|
|
|
// processPoints tests if the ring is convex, calculates the outer
|
|
|
|
// rectangle, and inserts segments into a boxtree in one pass.
|
2018-10-22 05:08:56 +03:00
|
|
|
func processPoints(points []Point, closed bool) (
|
2018-10-11 00:25:40 +03:00
|
|
|
convex bool, rect Rect, clockwise bool,
|
|
|
|
) {
|
|
|
|
if (closed && len(points) < 3) || len(points) < 2 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
var concave bool
|
|
|
|
var dir int
|
|
|
|
var a, b, c Point
|
|
|
|
var cwc float64
|
|
|
|
|
|
|
|
for i := 0; i < len(points); i++ {
|
|
|
|
// process the rectangle inflation
|
|
|
|
if i == 0 {
|
|
|
|
rect = Rect{points[i], points[i]}
|
|
|
|
} else {
|
|
|
|
if points[i].X < rect.Min.X {
|
|
|
|
rect.Min.X = points[i].X
|
|
|
|
} else if points[i].X > rect.Max.X {
|
|
|
|
rect.Max.X = points[i].X
|
|
|
|
}
|
|
|
|
if points[i].Y < rect.Min.Y {
|
|
|
|
rect.Min.Y = points[i].Y
|
|
|
|
} else if points[i].Y > rect.Max.Y {
|
|
|
|
rect.Max.Y = points[i].Y
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// gather some point positions for concave and clockwise detection
|
|
|
|
a = points[i]
|
|
|
|
if i == len(points)-1 {
|
|
|
|
b = points[0]
|
|
|
|
c = points[1]
|
|
|
|
} else if i == len(points)-2 {
|
|
|
|
b = points[i+1]
|
|
|
|
c = points[0]
|
|
|
|
} else {
|
|
|
|
b = points[i+1]
|
|
|
|
c = points[i+2]
|
|
|
|
}
|
|
|
|
|
|
|
|
// process the clockwise detection
|
|
|
|
cwc += (b.X - a.X) * (b.Y + a.Y)
|
|
|
|
|
|
|
|
// process the convex calculation
|
|
|
|
if concave {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
zCrossProduct := (b.X-a.X)*(c.Y-b.Y) - (b.Y-a.Y)*(c.X-b.X)
|
|
|
|
if dir == 0 {
|
|
|
|
if zCrossProduct < 0 {
|
|
|
|
dir = -1
|
|
|
|
} else if zCrossProduct > 0 {
|
|
|
|
dir = 1
|
|
|
|
}
|
|
|
|
} else if zCrossProduct < 0 {
|
|
|
|
if dir == 1 {
|
|
|
|
concave = true
|
|
|
|
}
|
|
|
|
} else if zCrossProduct > 0 {
|
|
|
|
if dir == -1 {
|
|
|
|
concave = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return !concave, rect, cwc > 0
|
|
|
|
}
|
2018-10-22 05:08:56 +03:00
|
|
|
|
|
|
|
func (series *baseSeries) clearIndex() {
|
|
|
|
series.index = nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (series *baseSeries) setCompressed(data []byte) {
|
|
|
|
binary.LittleEndian.PutUint32(data[1:], uint32(len(data)))
|
|
|
|
smaller := make([]byte, len(data))
|
|
|
|
copy(smaller, data)
|
|
|
|
// use the byte point instead of a double reference to the byte slice
|
|
|
|
series.index = &smaller[0]
|
|
|
|
}
|
|
|
|
|
|
|
|
func (series *baseSeries) buildIndex() {
|
|
|
|
if series.index != nil {
|
|
|
|
// already built
|
|
|
|
return
|
|
|
|
}
|
|
|
|
switch series.indexKind {
|
|
|
|
case RTree, RTreeCompressed:
|
|
|
|
tr := new(rTree)
|
|
|
|
n := series.NumSegments()
|
|
|
|
for i := 0; i < n; i++ {
|
|
|
|
rect := series.SegmentAt(i).Rect()
|
|
|
|
tr.Insert(
|
|
|
|
[]float64{rect.Min.X, rect.Min.Y},
|
|
|
|
[]float64{rect.Max.X, rect.Max.Y}, i)
|
|
|
|
}
|
|
|
|
if series.indexKind == RTreeCompressed {
|
|
|
|
series.setCompressed(
|
|
|
|
tr.compress([]byte{1, 0, 0, 0, 0}),
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
series.index = tr
|
|
|
|
}
|
|
|
|
case QuadTree, QuadTreeCompressed:
|
|
|
|
root := new(qNode)
|
|
|
|
n := series.NumSegments()
|
|
|
|
for i := 0; i < n; i++ {
|
|
|
|
seg := series.SegmentAt(i)
|
|
|
|
root.insert(series, series.rect, seg.Rect(), i, 0)
|
|
|
|
}
|
|
|
|
if series.indexKind == QuadTreeCompressed {
|
|
|
|
series.setCompressed(
|
|
|
|
root.compress([]byte{2, 0, 0, 0, 0}, series.rect),
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
series.index = root
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|