github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/geo/geogfn/segmentize.go (about)

     1  // Copyright 2020 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  package geogfn
    12  
    13  import (
    14  	"math"
    15  
    16  	"github.com/cockroachdb/cockroach/pkg/geo"
    17  	"github.com/cockroachdb/cockroach/pkg/geo/geographiclib"
    18  	"github.com/cockroachdb/errors"
    19  	"github.com/golang/geo/s2"
    20  	"github.com/twpayne/go-geom"
    21  )
    22  
    23  // Segmentize return modified Geography having no segment longer
    24  // that given maximum segment length.
    25  // This works by dividing each segment by a power of 2 to find the
    26  // smallest power less than or equal to the segmentMaxLength.
    27  func Segmentize(geography *geo.Geography, segmentMaxLength float64) (*geo.Geography, error) {
    28  	geometry, err := geography.AsGeomT()
    29  	if err != nil {
    30  		return nil, err
    31  	}
    32  	switch geometry := geometry.(type) {
    33  	case *geom.Point, *geom.MultiPoint:
    34  		return geography, nil
    35  	default:
    36  		if segmentMaxLength <= 0 {
    37  			return nil, errors.Newf("maximum segment length must be positive")
    38  		}
    39  		spheroid := geographiclib.WGS84Spheroid
    40  		// Convert segmentMaxLength to Angle with respect to earth sphere as
    41  		// further calculation is done considering segmentMaxLength as Angle.
    42  		segmentMaxAngle := segmentMaxLength / spheroid.SphereRadius
    43  		segGeometry, err := segmentizeGeom(geometry, segmentMaxAngle)
    44  		if err != nil {
    45  			return nil, err
    46  		}
    47  		return geo.NewGeographyFromGeom(segGeometry)
    48  	}
    49  }
    50  
    51  // segmentizeGeom returns a modified geom.T having no segment longer than
    52  // the given maximum segment length.
    53  func segmentizeGeom(geometry geom.T, segmentMaxAngle float64) (geom.T, error) {
    54  	if geometry.Empty() {
    55  		return geometry, nil
    56  	}
    57  	switch geometry := geometry.(type) {
    58  	case *geom.Point, *geom.MultiPoint:
    59  		return geometry, nil
    60  	case *geom.LineString:
    61  		var allFlatCoordinates []float64
    62  		for pointIdx := 1; pointIdx < geometry.NumCoords(); pointIdx++ {
    63  			allFlatCoordinates = append(
    64  				allFlatCoordinates,
    65  				segmentizeCoords(geometry.Coord(pointIdx-1), geometry.Coord(pointIdx), segmentMaxAngle)...,
    66  			)
    67  		}
    68  		// Appending end point as it wasn't included in the iteration of coordinates.
    69  		allFlatCoordinates = append(allFlatCoordinates, geometry.Coord(geometry.NumCoords()-1)...)
    70  		return geom.NewLineStringFlat(geom.XY, allFlatCoordinates).SetSRID(geometry.SRID()), nil
    71  	case *geom.MultiLineString:
    72  		segMultiLine := geom.NewMultiLineString(geom.XY).SetSRID(geometry.SRID())
    73  		for lineIdx := 0; lineIdx < geometry.NumLineStrings(); lineIdx++ {
    74  			l, err := segmentizeGeom(geometry.LineString(lineIdx), segmentMaxAngle)
    75  			if err != nil {
    76  				return nil, err
    77  			}
    78  			err = segMultiLine.Push(l.(*geom.LineString))
    79  			if err != nil {
    80  				return nil, err
    81  			}
    82  		}
    83  		return segMultiLine, nil
    84  	case *geom.LinearRing:
    85  		var allFlatCoordinates []float64
    86  		for pointIdx := 1; pointIdx < geometry.NumCoords(); pointIdx++ {
    87  			allFlatCoordinates = append(
    88  				allFlatCoordinates,
    89  				segmentizeCoords(geometry.Coord(pointIdx-1), geometry.Coord(pointIdx), segmentMaxAngle)...,
    90  			)
    91  		}
    92  		// Appending end point as it wasn't included in the iteration of coordinates.
    93  		allFlatCoordinates = append(allFlatCoordinates, geometry.Coord(geometry.NumCoords()-1)...)
    94  		return geom.NewLinearRingFlat(geom.XY, allFlatCoordinates).SetSRID(geometry.SRID()), nil
    95  	case *geom.Polygon:
    96  		segPolygon := geom.NewPolygon(geom.XY).SetSRID(geometry.SRID())
    97  		for loopIdx := 0; loopIdx < geometry.NumLinearRings(); loopIdx++ {
    98  			l, err := segmentizeGeom(geometry.LinearRing(loopIdx), segmentMaxAngle)
    99  			if err != nil {
   100  				return nil, err
   101  			}
   102  			err = segPolygon.Push(l.(*geom.LinearRing))
   103  			if err != nil {
   104  				return nil, err
   105  			}
   106  		}
   107  		return segPolygon, nil
   108  	case *geom.MultiPolygon:
   109  		segMultiPolygon := geom.NewMultiPolygon(geom.XY).SetSRID(geometry.SRID())
   110  		for polygonIdx := 0; polygonIdx < geometry.NumPolygons(); polygonIdx++ {
   111  			p, err := segmentizeGeom(geometry.Polygon(polygonIdx), segmentMaxAngle)
   112  			if err != nil {
   113  				return nil, err
   114  			}
   115  			err = segMultiPolygon.Push(p.(*geom.Polygon))
   116  			if err != nil {
   117  				return nil, err
   118  			}
   119  		}
   120  		return segMultiPolygon, nil
   121  	case *geom.GeometryCollection:
   122  		segGeomCollection := geom.NewGeometryCollection().SetSRID(geometry.SRID())
   123  		for geoIdx := 0; geoIdx < geometry.NumGeoms(); geoIdx++ {
   124  			g, err := segmentizeGeom(geometry.Geom(geoIdx), segmentMaxAngle)
   125  			if err != nil {
   126  				return nil, err
   127  			}
   128  			err = segGeomCollection.Push(g)
   129  			if err != nil {
   130  				return nil, err
   131  			}
   132  		}
   133  		return segGeomCollection, nil
   134  	}
   135  	return nil, errors.Newf("unknown type: %T", geometry)
   136  }
   137  
   138  // segmentizeCoords inserts multiple points between given two-coordinate and
   139  // return resultant points as flat []float64. Such that distance between any two
   140  // points is less than given maximum segment's length, the total number of
   141  // segments is the power of 2, and all the segments are of the same length.
   142  // NOTE: List of points does not consist of end point.
   143  func segmentizeCoords(a geom.Coord, b geom.Coord, segmentMaxAngle float64) []float64 {
   144  	// Converted geom.Coord into s2.Point so we can segmentize the coordinates.
   145  	pointA := s2.PointFromLatLng(s2.LatLngFromDegrees(a.Y(), a.X()))
   146  	pointB := s2.PointFromLatLng(s2.LatLngFromDegrees(b.Y(), b.X()))
   147  
   148  	allSegmentizedCoordinates := a.Clone()
   149  	chordAngleBetweenPoints := s2.ChordAngleBetweenPoints(pointA, pointB).Angle().Radians()
   150  	if segmentMaxAngle <= chordAngleBetweenPoints {
   151  		// This calculation is to determine the total number of segment between given
   152  		// 2 coordinates, ensuring that the segments are divided into parts divisible by
   153  		// a power of 2.
   154  		//
   155  		// For that fraction by segment must be less than or equal to
   156  		// the fraction of max segment length to distance between point, since the
   157  		// total number of segment must be power of 2 therefore we can write as
   158  		// 1 / (2^n)[numberOfSegmentToCreate] <= segmentMaxLength / distanceBetweenPoints < 1 / (2^(n-1))
   159  		// (2^n)[numberOfSegmentToCreate] >= distanceBetweenPoints / segmentMaxLength > 2^(n-1)
   160  		// therefore n = ceil(log2(segmentMaxLength/distanceBetweenPoints)). Hence
   161  		// numberOfSegmentToCreate = 2^(ceil(log2(segmentMaxLength/distanceBetweenPoints))).
   162  		numberOfSegmentToCreate := int(math.Pow(2, math.Ceil(math.Log2(chordAngleBetweenPoints/segmentMaxAngle))))
   163  		for pointInserted := 1; pointInserted < numberOfSegmentToCreate; pointInserted++ {
   164  			newPoint := s2.Interpolate(float64(pointInserted)/float64(numberOfSegmentToCreate), pointA, pointB)
   165  			latLng := s2.LatLngFromPoint(newPoint)
   166  			allSegmentizedCoordinates = append(allSegmentizedCoordinates, latLng.Lng.Degrees(), latLng.Lat.Degrees())
   167  		}
   168  	}
   169  	return allSegmentizedCoordinates
   170  }