mirror of
https://github.com/hanzoai/graph.git
synced 2026-10-30 18:35:37 +00:00
Derived from Dgraph (github.com/dgraph-io/dgraph) at commit 64804bd2, Apache-2.0. See NOTICE. WHAT THIS IS. The graph engine as a Go library: posting lists, the DQL parser, the type and tokenizer system, the index and schema layers, and the wire types. 39,170 lines of the upstream's 166,337. THE STORE is github.com/luxfi/zapdb, a Badger fork that kept the package name, the subpackages (options, pb, y) and the API — Open, OpenManaged, DB, Options — so the 74 import sites are aliased rather than rewritten. THE CODEC is ZAP. zapdb serializes with ZAP, not protobuf: its pb package builds the ZAP types by default and the protobuf ones only under `-tags grpc`, a path that does not compile (table/builder.go calls pb.Marshal, which that tag removes). One call site in posting needed converting; every other type here is Hanzo's own protobuf and is untouched. NO TRANSPORT. An embedded engine dials nothing, so nothing here imports google.golang.org/grpc. Removed: 20 transport functions in x, the generated service stubs for Raft, Zero and Worker (1,657 lines), and the JWT helper — identity belongs to IAM. The core called none of them. NOT KEPT: worker, conn, raftwal, the cluster commands, the GraphQL layer and the test harnesses. `query`, the DQL execution engine, went with them because it depends on worker; parsing DQL is available here, executing it is not. The history of the upstream project lives at github.com/dgraph-io/dgraph. This repository begins at the derivation because three quarters of that history describes code this fork does not carry.
227 lines
7.1 KiB
Go
227 lines
7.1 KiB
Go
/*
|
|
* SPDX-FileCopyrightText: © 2017-2026 Istari Digital, Inc.
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
package types
|
|
|
|
import (
|
|
"log"
|
|
|
|
"github.com/golang/geo/s2"
|
|
"github.com/pkg/errors"
|
|
geom "github.com/twpayne/go-geom"
|
|
|
|
"github.com/hanzoai/graph/x"
|
|
)
|
|
|
|
func parentCoverTokens(parents s2.CellUnion, cover s2.CellUnion) []string {
|
|
// We index parents and cover using different prefix, that makes it more
|
|
// performant at query time to only look up parents/cover depending on what
|
|
// kind of query it is.
|
|
tokens := make([]string, 0, len(parents)+len(cover))
|
|
tokens = append(tokens, createTokens(parents, parentPrefix)...)
|
|
tokens = append(tokens, createTokens(cover, coverPrefix)...)
|
|
x.AssertTruef(len(tokens) == len(parents)+len(cover), "%d %d %d",
|
|
len(tokens), len(parents), len(cover))
|
|
return tokens
|
|
}
|
|
|
|
// IndexGeoTokens returns the tokens to be used in a geospatial index for the
|
|
// given geometry. If the geometry is not supported it returns an error.
|
|
func IndexGeoTokens(g geom.T) ([]string, error) {
|
|
parents, cover, err := indexCells(g)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return parentCoverTokens(parents, cover), nil
|
|
}
|
|
|
|
// IndexKeysForCap returns the keys to be used in a geospatial index for a Cap.
|
|
/*
|
|
func indexCellsForCap(c s2.Cap) s2.CellUnion {
|
|
rc := &s2.RegionCoverer{
|
|
MinLevel: MinCellLevel,
|
|
MaxLevel: MaxCellLevel,
|
|
LevelMod: 0,
|
|
MaxCells: MaxCells,
|
|
}
|
|
return rc.Covering(c)
|
|
}
|
|
*/
|
|
|
|
const (
|
|
parentPrefix = "p/"
|
|
coverPrefix = "c/"
|
|
)
|
|
|
|
// IndexCells returns two cellunions. The first is a list of parents, which are all the cells upto
|
|
// the min level that contain this geometry. The second is the cover, which are the smallest
|
|
// possible cells required to cover the region. This makes it easier at query time to query only the
|
|
// parents or only the cover or both depending on whether it is a within, contains or intersects
|
|
// query.
|
|
func indexCells(g geom.T) (parents, cover s2.CellUnion, err error) {
|
|
if g.Stride() != 2 {
|
|
return nil, nil, errors.Errorf("Covering only available for 2D co-ordinates.")
|
|
}
|
|
switch v := g.(type) {
|
|
case *geom.Point:
|
|
p, c := indexCellsForPoint(v, MinCellLevel, MaxCellLevel)
|
|
return p, c, nil
|
|
case *geom.Polygon:
|
|
l, err := loopFromPolygon(v)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
cover := coverLoop(l, MinCellLevel, MaxCellLevel, MaxCells)
|
|
parents := getParentCells(cover, MinCellLevel)
|
|
return parents, cover, nil
|
|
case *geom.MultiPolygon:
|
|
var cover s2.CellUnion
|
|
// Convert each polygon to loop. Get cover for each and append to cover.
|
|
for i := range v.NumPolygons() {
|
|
p := v.Polygon(i)
|
|
l, err := loopFromPolygon(p)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
cover = append(cover, coverLoop(l, MinCellLevel, MaxCellLevel, MaxCells)...)
|
|
}
|
|
// Get parents for all cells in cover.
|
|
parents := getParentCells(cover, MinCellLevel)
|
|
return parents, cover, nil
|
|
default:
|
|
return nil, nil, errors.Errorf("Cannot index geometry of type %T", v)
|
|
}
|
|
}
|
|
|
|
const (
|
|
// MinCellLevel is the smallest cell level (largest cell size) used by indexing
|
|
MinCellLevel = 5 // Approx 250km x 380km
|
|
// MaxCellLevel is the largest cell level (smallest cell size) used by indexing
|
|
MaxCellLevel = 16 // Approx 120m x 180m
|
|
// MaxCells is the maximum number of cells to use when indexing regions.
|
|
MaxCells = 18
|
|
)
|
|
|
|
func pointFromCoord(r geom.Coord) s2.Point {
|
|
// The geojson spec says that coordinates are specified as [long, lat]
|
|
// We assume that any data encoded in the database follows that format.
|
|
ll := s2.LatLngFromDegrees(r.Y(), r.X())
|
|
return s2.PointFromLatLng(ll)
|
|
}
|
|
|
|
// PointFromPoint converts a geom.Point to a s2.Point
|
|
func pointFromPoint(p *geom.Point) s2.Point {
|
|
return pointFromCoord(p.Coords())
|
|
}
|
|
|
|
// loopFromPolygon converts a geom.Polygon to a s2.Loop. We use loops instead of s2.Polygon as the
|
|
// s2.Polygon implemention is incomplete.
|
|
func loopFromPolygon(p *geom.Polygon) (*s2.Loop, error) {
|
|
// go implementation of s2 does not support more than one loop (and will panic if the size of
|
|
// the loops array > 1). So we will skip the holes in the polygon and just use the outer loop.
|
|
r := p.LinearRing(0)
|
|
n := r.NumCoords()
|
|
if n < 4 {
|
|
return nil, errors.Errorf("Can't convert ring with less than 4 pts")
|
|
}
|
|
if !r.Coord(0).Equal(geom.XY, r.Coord(n-1)) {
|
|
return nil, errors.Errorf("Last coordinate not same as first for polygon: %+v\n", p)
|
|
}
|
|
// S2 specifies that the orientation of the polygons should be CCW. However there is no
|
|
// restriction on the orientation in WKB (or geojson). To get the correct orientation we assume
|
|
// that the polygons are always less than one hemisphere. If they are bigger, we flip the
|
|
// orientation.
|
|
reverse := isClockwise(r)
|
|
l := loopFromRing(r, reverse)
|
|
|
|
// Since our clockwise check was approximate, we check the cap and reverse if needed.
|
|
if l.CapBound().Radius().Degrees() > 90 {
|
|
l.Invert()
|
|
}
|
|
return l, nil
|
|
}
|
|
|
|
// Checks if a ring is clockwise or counter-clockwise. Note: This uses the algorithm for planar
|
|
// polygons and doesn't work for spherical polygons that contain the poles or the antimeridan
|
|
// discontinuity. We use this as a fast approximation instead.
|
|
func isClockwise(r *geom.LinearRing) bool {
|
|
// The algorithm is described here https://en.wikipedia.org/wiki/Shoelace_formula
|
|
var a float64
|
|
n := r.NumCoords()
|
|
for i := range n {
|
|
p1 := r.Coord(i)
|
|
p2 := r.Coord((i + 1) % n)
|
|
a += (p2.X() - p1.X()) * (p1.Y() + p2.Y())
|
|
}
|
|
return a > 0
|
|
}
|
|
|
|
func loopFromRing(r *geom.LinearRing, reverse bool) *s2.Loop {
|
|
// In WKB, the last coordinate is repeated for a ring to form a closed loop. For s2 the points
|
|
// aren't allowed to repeat and the loop is assumed to be closed, so we skip the last point.
|
|
n := r.NumCoords()
|
|
pts := make([]s2.Point, n-1)
|
|
for i := range n - 1 {
|
|
var c geom.Coord
|
|
if reverse {
|
|
c = r.Coord(n - 1 - i)
|
|
} else {
|
|
c = r.Coord(i)
|
|
}
|
|
p := pointFromCoord(c)
|
|
pts[i] = p
|
|
}
|
|
return s2.LoopFromPoints(pts)
|
|
}
|
|
|
|
// create cells for point from the minLevel to maxLevel both inclusive.
|
|
func indexCellsForPoint(p *geom.Point, minLevel, maxLevel int) (s2.CellUnion, s2.CellUnion) {
|
|
if maxLevel < minLevel {
|
|
log.Fatalf("Maxlevel should be greater than minLevel")
|
|
}
|
|
ll := s2.LatLngFromDegrees(p.Y(), p.X())
|
|
c := s2.CellIDFromLatLng(ll)
|
|
cells := make([]s2.CellID, maxLevel-minLevel+1)
|
|
for l := minLevel; l <= maxLevel; l++ {
|
|
cells[l-minLevel] = c.Parent(l)
|
|
}
|
|
return cells, []s2.CellID{c.Parent(maxLevel)}
|
|
}
|
|
|
|
func getParentCells(cu s2.CellUnion, minLevel int) s2.CellUnion {
|
|
parents := make(map[s2.CellID]bool)
|
|
for _, c := range cu {
|
|
for l := c.Level(); l >= minLevel; l-- {
|
|
parents[c.Parent(l)] = true
|
|
}
|
|
}
|
|
// convert the parents map to an array
|
|
cells := make([]s2.CellID, len(parents))
|
|
i := 0
|
|
for k := range parents {
|
|
cells[i] = k
|
|
i++
|
|
}
|
|
return cells
|
|
}
|
|
|
|
func coverLoop(l *s2.Loop, minLevel int, maxLevel int, maxCells int) s2.CellUnion {
|
|
rc := &s2.RegionCoverer{
|
|
MinLevel: minLevel,
|
|
MaxLevel: maxLevel,
|
|
LevelMod: 0,
|
|
MaxCells: maxCells,
|
|
}
|
|
return rc.Covering(l)
|
|
}
|
|
|
|
// appendTokens creates tokens with a certain prefix and append.
|
|
func createTokens(cu s2.CellUnion, prefix string) (toks []string) {
|
|
for _, c := range cu {
|
|
toks = append(toks, prefix+c.ToToken())
|
|
}
|
|
return toks
|
|
}
|