From 2dcf57d4c98c262524bee22a6fb0f011961f838b Mon Sep 17 00:00:00 2001 From: Dirk van Bree Date: Wed, 8 Jul 2026 10:31:35 +0200 Subject: [PATCH 01/27] feat: integer polygons with conversion to floating-point --- intgeom/polygon.go | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 intgeom/polygon.go diff --git a/intgeom/polygon.go b/intgeom/polygon.go new file mode 100644 index 0000000..f6f47b7 --- /dev/null +++ b/intgeom/polygon.go @@ -0,0 +1,29 @@ +package intgeom + +import ( + "github.com/go-spatial/geom" +) + +type Polygon [][][2]M + +func (p Polygon) ToGeomPolygon() geom.Polygon { + geompol := make([][][2]float64, len(p)) + for i, ring := range p { + geompol[i] = make([][2]float64, len(ring)) + for j, point := range ring { + geompol[i][j] = [2]float64{ToGeomOrd(point[0]), ToGeomOrd(point[1])} + } + } + return geompol +} + +func FromGeomPolygon(p geom.Polygon) Polygon { + intpol := make([][][2]int64, len(p)) + for i, ring := range p { + intpol[i] = make([][2]int64, len(ring)) + for j, point := range ring { + intpol[i][j] = [2]int64{FromGeomOrd(point[0]), FromGeomOrd(point[1])} + } + } + return intpol +} From 8d9ac55a1b0327e7cc7274b97ea3793d18d00359 Mon Sep 17 00:00:00 2001 From: Dirk van Bree Date: Wed, 8 Jul 2026 11:53:58 +0200 Subject: [PATCH 02/27] feat: copy pbf encoding functionality from go-spatial --- tile/go_spatial_feature.go | 755 +++++++++++++++++++++++++++++++++++++ 1 file changed, 755 insertions(+) create mode 100644 tile/go_spatial_feature.go diff --git a/tile/go_spatial_feature.go b/tile/go_spatial_feature.go new file mode 100644 index 0000000..7ba1342 --- /dev/null +++ b/tile/go_spatial_feature.go @@ -0,0 +1,755 @@ +package tile + +// This file was copied from go-spatial/geom/encoding/mvt/feature.go +// Modifications made: +// - Made EncodeGeometry an exported function. +// - Made file self-contained by adding type definitions from +// go-spatial/geom/encoding/mvt/vector_tile/vector_tile.pb.go +// - Made file self-contained by defining the debug variable + +import ( + "context" + "fmt" + "log" + + "github.com/go-spatial/geom" + "github.com/go-spatial/geom/encoding/wkt" + "github.com/go-spatial/geom/winding" +) + +// START copied from vector_tile.pb.go + +type Tile_GeomType int32 + +const ( + Tile_UNKNOWN Tile_GeomType = 0 + Tile_POINT Tile_GeomType = 1 + Tile_LINESTRING Tile_GeomType = 2 + Tile_POLYGON Tile_GeomType = 3 +) + +var Tile_GeomType_name = map[int32]string{ + 0: "UNKNOWN", + 1: "POINT", + 2: "LINESTRING", + 3: "POLYGON", +} +var Tile_GeomType_value = map[string]int32{ + "UNKNOWN": 0, + "POINT": 1, + "LINESTRING": 2, + "POLYGON": 3, +} +var ( + ErrNilFeature = fmt.Errorf("feature is nil") + ErrUnknownGeometryType = fmt.Errorf("unknown geometry type") + ErrNilGeometryType = fmt.Errorf("geometry is nil") +) + +type Tile_Feature struct { + Id *uint64 `protobuf:"varint,1,opt,name=id,def=0" json:"id,omitempty"` + // Tags of this feature are encoded as repeated pairs of + // integers. + // A detailed description of tags is located in sections + // 4.2 and 4.4 of the specification + Tags []uint32 `protobuf:"varint,2,rep,packed,name=tags" json:"tags,omitempty"` + // The type of geometry stored in this feature. + Type *Tile_GeomType `protobuf:"varint,3,opt,name=type,enum=vector_tile.Tile_GeomType,def=0" json:"type,omitempty"` + // Contains a stream of commands and parameters (vertices). + // A detailed description on geometry encoding is located in + // section 4.3 of the specification. + Geometry []uint32 `protobuf:"varint,4,rep,packed,name=geometry" json:"geometry,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +// END copied from vector_tile.pb.go + +// Definition needed for compilation +const debug = false + +// Rest of file is copied from feature.go + + +// TODO: Need to put in validation for the Geometry, as current the system +// does not check to make sure that the geometry is following the rules as +// laid out by the spec (i.e. polygons must not have the same start and end +// point). + +// Feature describes a feature of a Layer. A layer will contain multiple features +// each of which has a geometry describing the interesting thing, and the metadata +// associated with it. +type Feature struct { + ID *uint64 + Tags map[string]interface{} + Geometry geom.Geometry +} + +func (f Feature) String() string { + g, err := wkt.EncodeString(f.Geometry) + if err != nil { + return fmt.Sprintf("encoding error for geom geom, %v", err) + } + + if f.ID != nil { + return fmt.Sprintf("{Feature: %v, GEO: %v, Tags: %+v}", *f.ID, g, f.Tags) + } + + return fmt.Sprintf("{Feature: GEO: %v, Tags: %+v}", g, f.Tags) +} + +// NewFeatures returns one or more features for the given Geometry +func NewFeatures(geo geom.Geometry, tags map[string]interface{}) (f []Feature) { + if geo == nil { + return f // return empty feature set for a nil geometry + } + + if g, ok := geo.(geom.Collection); ok { + geos := g.Geometries() + for i := range geos { + f = append(f, NewFeatures(geos[i], tags)...) + } + return f + } + + f = append(f, Feature{ + Tags: tags, + Geometry: geo, + }) + + return f +} + +// VTileFeature will return a vectorTile.Feature that would represent the Feature +func (f *Feature) VTileFeature(ctx context.Context, keys []string, vals []interface{}) (tf *Tile_Feature, err error) { + tf = new(Tile_Feature) + tf.Id = f.ID + + if tf.Tags, err = keyvalTagsMap(keys, vals, f); err != nil { + return tf, err + } + + geo, gtype, err := EncodeGeometry(ctx, f.Geometry) + if err != nil { + return tf, err + } + + if len(geo) == 0 { + return nil, nil + } + + tf.Geometry = geo + tf.Type = >ype + + return tf, nil +} + +// These values came from: https://github.com/mapbox/vector-tile-spec/tree/master/2.1 +const ( + cmdMoveTo uint32 = 1 + cmdLineTo uint32 = 2 + cmdClosePath uint32 = 7 + + maxCmdCount uint32 = 0x1FFFFFFF +) + +type Command uint32 + +// NewCommand return a new command encoder +func NewCommand(cmd uint32, count int) Command { + return Command((cmd & 0x7) | (uint32(count) << 3)) +} + +//ID encodes the ID of the command +func (c Command) ID() uint32 { + return uint32(c) & 0x7 +} + +//Count encode the count of elements in the command +func (c Command) Count() int { + return int(uint32(c) >> 3) +} + +func (c Command) String() string { + switch c.ID() { + case cmdMoveTo: + return fmt.Sprintf("move Command with count %v", c.Count()) + case cmdLineTo: + return fmt.Sprintf("line To command with count %v", c.Count()) + case cmdClosePath: + return fmt.Sprintf("close path command with count %v", c.Count()) + default: + return fmt.Sprintf("unknown command (%v) with count %v", c.ID(), c.Count()) + } +} + +// encodeZigZag does the ZigZag encoding for small ints. +func encodeZigZag(i int64) uint32 { + return uint32((i << 1) ^ (i >> 31)) +} + +// cursor reprsents the current position, this is needed to encode the geometry. +// the origin (0,0) is the top-left of the Tile. +type cursor struct { + // The coordinates — these should be int64, when they were float64 they + // introduced a slight drift in the coordinates. + x int64 + y int64 +} + +// NewCursor creates a new cursor for drawing and MVT tile +func NewCursor() *cursor { + return &cursor{} +} + +// GetDeltaPointAndUpdate returns the delta of for the given point from the current +// cursor position +func (c *cursor) GetDeltaPointAndUpdate(p geom.Point) (dx, dy int64) { + delta := c.moveCursorPoints([2]int64{int64(p.X()), int64(p.Y())}) + return delta[0][0], delta[0][1] +} + +func (c *cursor) moveCursorPoints(pts ...[2]int64) (deltas [][2]int64) { + deltas = make([][2]int64, len(pts)) + for i := range pts { + deltas[i][0] = pts[i][0] - c.x + deltas[i][1] = pts[i][1] - c.y + c.x, c.y = pts[i][0], pts[i][1] + } + return deltas +} + +func (c *cursor) encodeZigZagPt(pts [][2]int64) []uint32 { + g := make([]uint32, 0, (2 * len(pts))) + for _, dp := range pts { + g = append(g, encodeZigZag(dp[0]), encodeZigZag(dp[1])) + } + return g +} + +func (c *cursor) encodeCmd(cmd uint32, points [][2]float64) []uint32 { + if len(points) == 0 { + return []uint32{} + } + // new slice to hold our encode bytes. 2 bytes for each point pluse a command byte. + g := make([]uint32, 0, (2*len(points))+1) + // add the command integer + g = append(g, cmd) + + // range through our points + for _, p := range points { + dx, dy := c.GetDeltaPointAndUpdate(geom.Point(p)) + // encode our delta point + g = append(g, encodeZigZag(dx), encodeZigZag(dy)) + } + + return g +} + +func (c *cursor) encodeLinearRing(order winding.Order, wo winding.Winding, ring [][2]float64) []uint32 { + + iring := make([][2]int64, len(ring)) + for i := range iring { + // the process of truncating the float can cause the winding order to flip! + iring[i][0], iring[i][1] = int64(ring[i][0]), int64(ring[i][1]) + } + ringWinding := order.OfInt64Points(iring...) + + if ringWinding.IsColinear() { + return []uint32{} + } + + if ringWinding != wo { + if debug { + log.Printf("(0) RING WKT:\n%v", wkt.MustEncode(geom.LineString(ring))) + log.Printf("(1) winding order: \n\tpts: %v\n\two : %v", ringWinding, wo) + } + // need to reverse the points in the ring + for i := len(iring)/2 - 1; i >= 0; i-- { + opp := len(iring) - 1 - i + iring[i], iring[opp] = iring[opp], iring[i] + } + if debug { + log.Printf("(2) RING WKT:\n%v", wkt.MustEncode(geom.LineString(ring))) + log.Printf("(2) winding order: \n\tpts: %v\n\two : %v", ringWinding, wo) + } + } + + deltas := c.moveCursorPoints(iring...) + + // 3 is for the three commands that it takes to describe a ring: move to, line to, and close + g := make([]uint32, 0, (2*len(iring))+3) + + // move to first point + g = append(g, + uint32(NewCommand(cmdMoveTo, 1)), + encodeZigZag(deltas[0][0]), + encodeZigZag(deltas[0][1]), + ) + + // line to each of the other points + g = append(g, uint32(NewCommand(cmdLineTo, len(deltas)-1))) + g = append(g, c.encodeZigZagPt(deltas[1:])...) + + // Close path + g = append(g, uint32(NewCommand(cmdClosePath, 1))) + + return g +} + +func (c *cursor) encodePolygon(geo geom.Polygon) []uint32 { + g := []uint32{} + + lines := geo.LinearRings() + for i := range lines { + // bail if number of points is less than two + if len(lines[i]) < 2 { + if i != 0 { + continue + } + return g + } + + // https://github.com/mapbox/vector-tile-spec/tree/master/2.1#4344-polygon-geometry-type + // An exterior ring is DEFINED as a linear ring having a positive area + // as calculated by applying the surveyor's formula to the vertices of + // the polygon in tile coordinates. In the tile coordinate system (with + // the Y axis positive down and X axis positive to the right) this makes + // the exterior ring's winding order appear clockwise. + // + // An interior ring is DEFINED as a linear ring having a negative area as + // calculated by applying the surveyor's formula to the vertices of the + // polygon in tile coordinates. In the tile coordinate system (with the + // Y axis positive down and X axis positive to the right) this makes the + // interior ring's winding order appear counterclockwise. + order := winding.Order{YPositiveDown: true} + wo := winding.CounterClockwise + if i == 0 { + wo = winding.Clockwise + } + g = append(g, c.encodeLinearRing(order, wo, lines[i])...) + } + return g +} + +// MoveTo encodes a move to command for the given points +func (c *cursor) MoveTo(points ...[2]float64) []uint32 { + return c.encodeCmd(uint32(NewCommand(cmdMoveTo, len(points))), points) +} + +// LineTo encodes a line to command for the given points +func (c *cursor) LineTo(points ...[2]float64) []uint32 { + return c.encodeCmd(uint32(NewCommand(cmdLineTo, len(points))), points) +} + +// ClosePath encodes a close path command +func (c *cursor) ClosePath() uint32 { + return uint32(NewCommand(cmdClosePath, 1)) +} + +// EncodeGeometry will take a geom.Geometry and encode it according to the +// mapbox vector_tile spec. +func EncodeGeometry(ctx context.Context, geometry geom.Geometry) (g []uint32, vtyp Tile_GeomType, err error) { + if geometry == nil { + return nil, Tile_UNKNOWN, ErrNilGeometryType + } + + c := NewCursor() + + switch t := geometry.(type) { + case geom.Point: + g = append(g, c.MoveTo(t)...) + return g, Tile_POINT, nil + + case geom.MultiPoint: + g = append(g, c.MoveTo(t.Points()...)...) + return g, Tile_POINT, nil + + case geom.LineString: + points := t.Vertices() + g = append(g, c.MoveTo(points[0])...) + g = append(g, c.LineTo(points[1:]...)...) + return g, Tile_LINESTRING, nil + + case geom.MultiLineString: + lines := t.LineStrings() + for _, l := range lines { + points := geom.LineString(l).Vertices() + g = append(g, c.MoveTo(points[0])...) + g = append(g, c.LineTo(points[1:]...)...) + } + return g, Tile_LINESTRING, nil + + case geom.Polygon: + g = append(g, c.encodePolygon(t)...) + return g, Tile_POLYGON, nil + + case geom.MultiPolygon: + polygons := t.Polygons() + for _, p := range polygons { + g = append(g, c.encodePolygon(p)...) + } + return g, Tile_POLYGON, nil + + case *geom.MultiPolygon: + if t == nil { + return g, Tile_POLYGON, nil + } + + polygons := t.Polygons() + for _, p := range polygons { + g = append(g, c.encodePolygon(p)...) + } + return g, Tile_POLYGON, nil + + default: + return nil, Tile_UNKNOWN, ErrUnknownGeometryType + } +} + +// keyvalMapsFromFeatures returns a key map and value map, to help with the translation +// to mapbox tile format. In the Tile format, the Tile contains a mapping of all the unique +// keys and values, and then each feature contains a vector map to these two. This is an +// intermediate data structure to help with the construction of the three mappings. +func keyvalMapsFromFeatures(features []Feature) (keyMap []string, valMap []interface{}, err error) { + var didFind bool + for _, f := range features { + for k, v := range f.Tags { + didFind = false + for _, mk := range keyMap { + if k == mk { + didFind = true + break + } + } + if !didFind { + keyMap = append(keyMap, k) + } + didFind = false + + switch vt := v.(type) { + default: + if vt == nil { + // ignore nil types + continue + } + return keyMap, valMap, fmt.Errorf("unsupported type for value(%v) with key(%v) in tags for feature %v.", vt, k, f) + + case string: + for _, mv := range valMap { + tmv, ok := mv.(string) + if !ok { + continue + } + if tmv == vt { + didFind = true + break + } + } + + case fmt.Stringer: + for _, mv := range valMap { + tmv, ok := mv.(fmt.Stringer) + if !ok { + continue + } + if tmv.String() == vt.String() { + didFind = true + break + } + } + + case int: + for _, mv := range valMap { + tmv, ok := mv.(int) + if !ok { + continue + } + if tmv == vt { + didFind = true + break + } + } + + case int8: + for _, mv := range valMap { + tmv, ok := mv.(int8) + if !ok { + continue + } + if tmv == vt { + didFind = true + break + } + } + + case int16: + for _, mv := range valMap { + tmv, ok := mv.(int16) + if !ok { + continue + } + if tmv == vt { + didFind = true + break + } + } + + case int32: + for _, mv := range valMap { + tmv, ok := mv.(int32) + if !ok { + continue + } + if tmv == vt { + didFind = true + break + } + } + + case int64: + for _, mv := range valMap { + tmv, ok := mv.(int64) + if !ok { + continue + } + if tmv == vt { + didFind = true + break + } + } + + case uint: + for _, mv := range valMap { + tmv, ok := mv.(uint) + if !ok { + continue + } + if tmv == vt { + didFind = true + break + } + } + + case uint8: + for _, mv := range valMap { + tmv, ok := mv.(uint8) + if !ok { + continue + } + if tmv == vt { + didFind = true + break + } + } + + case uint16: + for _, mv := range valMap { + tmv, ok := mv.(uint16) + if !ok { + continue + } + if tmv == vt { + didFind = true + break + } + } + + case uint32: + for _, mv := range valMap { + tmv, ok := mv.(uint32) + if !ok { + continue + } + if tmv == vt { + didFind = true + break + } + } + + case uint64: + for _, mv := range valMap { + tmv, ok := mv.(uint64) + if !ok { + continue + } + if tmv == vt { + didFind = true + break + } + } + + case float32: + for _, mv := range valMap { + tmv, ok := mv.(float32) + if !ok { + continue + } + if tmv == vt { + didFind = true + break + } + } + + case float64: + for _, mv := range valMap { + tmv, ok := mv.(float64) + if !ok { + continue + } + if tmv == vt { + didFind = true + break + } + } + + case bool: + for _, mv := range valMap { + tmv, ok := mv.(bool) + if !ok { + continue + } + if tmv == vt { + didFind = true + break + } + } + + } // value type switch + + if !didFind { + valMap = append(valMap, v) + } + + } // For f.Tags + } // for features + return keyMap, valMap, nil +} + +// keyvalTagsMap will return the tags map as expected by the mapbox tile spec. It takes +// a keyMap and a valueMap that list the the order of the expected keys and values. It will +// return a vector map that refers to these two maps. +func keyvalTagsMap(keyMap []string, valueMap []interface{}, f *Feature) (tags []uint32, err error) { + + if f == nil { + return nil, ErrNilFeature + } + + var kidx, vidx int64 + + for key, val := range f.Tags { + + kidx, vidx = -1, -1 // Set to known not found value. + + for i, k := range keyMap { + if k != key { + continue // move to the next key + } + kidx = int64(i) + break // we found a match + } + + if kidx == -1 { + log.Printf("did not find key (%v) in keymap.", key) + return tags, fmt.Errorf("did not find key (%v) in keymap.", key) + } + + // if val is nil we skip it for now + // https://github.com/mapbox/vector-tile-spec/issues/62 + if val == nil { + continue + } + + for i, v := range valueMap { + switch tv := val.(type) { + default: + return tags, fmt.Errorf("value (%[1]v) of type (%[1]T) for key (%[2]v) is not supported.", tv, key) + case string: + vmt, ok := v.(string) // Make sure the type of the Value map matches the type of the Tag's value + if !ok || vmt != tv { // and that the values match + continue // if they don't match move to the next value. + } + case fmt.Stringer: + vmt, ok := v.(fmt.Stringer) + if !ok || vmt.String() != tv.String() { + continue + } + case int: + vmt, ok := v.(int) + if !ok || vmt != tv { + continue + } + case int8: + vmt, ok := v.(int8) + if !ok || vmt != tv { + continue + } + case int16: + vmt, ok := v.(int16) + if !ok || vmt != tv { + continue + } + case int32: + vmt, ok := v.(int32) + if !ok || vmt != tv { + continue + } + case int64: + vmt, ok := v.(int64) + if !ok || vmt != tv { + continue + } + case uint: + vmt, ok := v.(uint) + if !ok || vmt != tv { + continue + } + case uint8: + vmt, ok := v.(uint8) + if !ok || vmt != tv { + continue + } + case uint16: + vmt, ok := v.(uint16) + if !ok || vmt != tv { + continue + } + case uint32: + vmt, ok := v.(uint32) + if !ok || vmt != tv { + continue + } + case uint64: + vmt, ok := v.(uint64) + if !ok || vmt != tv { + continue + } + + case float32: + vmt, ok := v.(float32) + if !ok || vmt != tv { + continue + } + case float64: + vmt, ok := v.(float64) + if !ok || vmt != tv { + continue + } + case bool: + vmt, ok := v.(bool) + if !ok || vmt != tv { + continue + } + } // Values Switch Statement + // if the values match let's record the index. + vidx = int64(i) + break // we found our value no need to continue on. + } // range on value + + if vidx == -1 { // None of the values matched. + return tags, fmt.Errorf("did not find a value: %v in valuemap.", val) + } + tags = append(tags, uint32(kidx), uint32(vidx)) + } // Move to the next tag key and value. + + return tags, nil +} From 6abcd3f1173cb93b80cb55442f7136f63d5078a3 Mon Sep 17 00:00:00 2001 From: Dirk van Bree Date: Wed, 8 Jul 2026 12:03:55 +0200 Subject: [PATCH 03/27] refactor: remove unused parameter in pbf encoding functionality --- tile/go_spatial_feature.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tile/go_spatial_feature.go b/tile/go_spatial_feature.go index 7ba1342..155395e 100644 --- a/tile/go_spatial_feature.go +++ b/tile/go_spatial_feature.go @@ -2,13 +2,13 @@ package tile // This file was copied from go-spatial/geom/encoding/mvt/feature.go // Modifications made: -// - Made EncodeGeometry an exported function. +// - Made EncodeGeometry an exported function. // - Made file self-contained by adding type definitions from // go-spatial/geom/encoding/mvt/vector_tile/vector_tile.pb.go // - Made file self-contained by defining the debug variable +// - Removed unused parameter "context" from all functions. import ( - "context" "fmt" "log" @@ -120,7 +120,7 @@ func NewFeatures(geo geom.Geometry, tags map[string]interface{}) (f []Feature) { } // VTileFeature will return a vectorTile.Feature that would represent the Feature -func (f *Feature) VTileFeature(ctx context.Context, keys []string, vals []interface{}) (tf *Tile_Feature, err error) { +func (f *Feature) VTileFeature(keys []string, vals []interface{}) (tf *Tile_Feature, err error) { tf = new(Tile_Feature) tf.Id = f.ID @@ -128,7 +128,7 @@ func (f *Feature) VTileFeature(ctx context.Context, keys []string, vals []interf return tf, err } - geo, gtype, err := EncodeGeometry(ctx, f.Geometry) + geo, gtype, err := EncodeGeometry(f.Geometry) if err != nil { return tf, err } @@ -348,7 +348,7 @@ func (c *cursor) ClosePath() uint32 { // EncodeGeometry will take a geom.Geometry and encode it according to the // mapbox vector_tile spec. -func EncodeGeometry(ctx context.Context, geometry geom.Geometry) (g []uint32, vtyp Tile_GeomType, err error) { +func EncodeGeometry(geometry geom.Geometry) (g []uint32, vtyp Tile_GeomType, err error) { if geometry == nil { return nil, Tile_UNKNOWN, ErrNilGeometryType } From f42a4a645d3d2f4cdc7a2921adf748bd50eecf24 Mon Sep 17 00:00:00 2001 From: Dirk van Bree Date: Wed, 8 Jul 2026 15:26:49 +0200 Subject: [PATCH 04/27] feat: encode polygon in quadrant --- tile/tile.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 tile/tile.go diff --git a/tile/tile.go b/tile/tile.go new file mode 100644 index 0000000..518aba7 --- /dev/null +++ b/tile/tile.go @@ -0,0 +1,30 @@ +package tile + +import ( + "github.com/go-spatial/geom" + "github.com/pdok/texel/pointindex" +) + +func translateToTileCoord(q pointindex.Quadrant, p geom.Polygon) geom.Polygon { + translatedPol := make(geom.Polygon, len(p)) + basepoint := q.Centroid() + + for i, ring := range p.LinearRings() { + translatedPol[i] = make([][2]float64, len(ring)) + for j, point := range ring { + translatedPol[i][j] = [2]float64{point[0] - basepoint[0], point[1] - basepoint[1]} + } + } + + return p +} + +func EncodePolygon(q pointindex.Quadrant, p geom.Polygon) ([]uint32, int32) { + translatedPol := translateToTileCoord(q, p) + encgeom, geomtype, err := EncodeGeometry(translatedPol) + if err != nil { + panic(err) + } + + return encgeom, int32(geomtype) +} From e96c81ce2580761eb0c97dfb07d15b98dc42bff5 Mon Sep 17 00:00:00 2001 From: Dirk van Bree Date: Wed, 8 Jul 2026 16:19:15 +0200 Subject: [PATCH 05/27] fix: prepare geo instead of just translation --- go.mod | 9 ++++++-- go.sum | 44 ++++++++++++++++++---------------------- pointindex/pointindex.go | 4 ++++ tile/tile.go | 24 +++++++++------------- 4 files changed, 41 insertions(+), 40 deletions(-) diff --git a/go.mod b/go.mod index 89a2d74..49ff554 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/carlmjohnson/versioninfo v0.22.5 github.com/creasty/defaults v1.8.0 github.com/go-playground/validator/v10 v10.30.3 - github.com/go-spatial/geom v0.0.0-20220918193402-3cd2f5a9a082 + github.com/go-spatial/geom v0.1.0 github.com/iancoleman/strcase v0.3.0 github.com/muesli/reflow v0.3.0 github.com/perimeterx/marshmallow v1.1.5 @@ -18,6 +18,7 @@ require ( ) require ( + github.com/arolek/p v0.0.0-20191103215535-df3c295ed582 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect @@ -26,11 +27,14 @@ require ( github.com/gdey/errors v0.0.0-20190426172550-8ebd5bc891fb // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-spatial/proj v0.3.0 // indirect + github.com/go-test/deep v1.1.1 // indirect + github.com/golang/protobuf v1.5.3 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-runewidth v0.0.12 // indirect - github.com/mattn/go-sqlite3 v1.14.17 // indirect + github.com/mattn/go-sqlite3 v1.14.22 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect @@ -38,5 +42,6 @@ require ( golang.org/x/crypto v0.52.0 // indirect golang.org/x/sys v0.45.0 // indirect golang.org/x/text v0.37.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 4d8f73b..1c9ce50 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +github.com/arolek/p v0.0.0-20191103215535-df3c295ed582 h1:DugKk4B3PpqfZ1QunDSPqNZDAErQmdCgwurloi1Axaw= github.com/arolek/p v0.0.0-20191103215535-df3c295ed582/go.mod h1:JPNItmi3yb44Q5QWM+Kh5n9oeRhfcJzPNS90mbLo25U= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= @@ -9,7 +10,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3 github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creasty/defaults v1.8.0 h1:z27FJxCAa0JKt3utc0sCImAEb+spPucmKoOdLHvHYKk= github.com/creasty/defaults v1.8.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= @@ -24,14 +24,18 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.30.3 h1:4MU6YkEwx7GbcPJOZxrtbu+QfF3pJLJuaYTeAH0DYy8= github.com/go-playground/validator/v10 v10.30.3/go.mod h1:4Axh7oCNGcoGkqLoE4YWt6n20mcEIsPRlB7vPk3lpyc= -github.com/go-spatial/geom v0.0.0-20220918193402-3cd2f5a9a082 h1:3+Swhq7I1stScm1DE4PUWKjGJRbi+VPvisj6tFz0prs= -github.com/go-spatial/geom v0.0.0-20220918193402-3cd2f5a9a082/go.mod h1:YU06tBGGstQCUX7vMuyF44RRneTdjGxOrp8OJHu4t9Q= -github.com/go-spatial/proj v0.2.0 h1:sii5Now3GFEyR9hV2SBJFWFz95s4xSaZmP0aYDk1K6c= -github.com/go-spatial/proj v0.2.0/go.mod h1:ePHHp7ITVc4eIVW5sgG/0Eu9RMAGOQUgM/D1ZkccY+0= -github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= -github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/go-spatial/geom v0.1.0 h1:IZ6LVz0Kkgd8+U83MmI0J4L4TBDzk5I8xiYe9Ci+aHk= +github.com/go-spatial/geom v0.1.0/go.mod h1:yNr22dnkQyFEwq2MJVmmiReiFesQpXKlosAG+qUSPus= +github.com/go-spatial/proj v0.3.0 h1:2IRb4elpnmVy1tUKRGBR68uiyZU4Pmny8yZC7TEUdSg= +github.com/go-spatial/proj v0.3.0/go.mod h1:wF0V0A60cTD5uzXeg+P52E1GtMY40PIiyYNJVvHXm6k= +github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= +github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -42,13 +46,10 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-sqlite3 v1.12.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= -github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= -github.com/mattn/goveralls v0.0.3-0.20180319021929-1c14a4061c1c/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= -github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -58,8 +59,6 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tobshub/go-sortedmap v1.0.3 h1:oUhj/5tqzjTX4bhWqB1ZFTDtMULJ1ZYUnS8WAugSfjY= @@ -72,23 +71,20 @@ github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/ github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M= golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= -golang.org/x/tools v0.0.0-20191114222411-4191b8cbba09/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pointindex/pointindex.go b/pointindex/pointindex.go index 52f7f39..f2b0e77 100644 --- a/pointindex/pointindex.go +++ b/pointindex/pointindex.go @@ -42,6 +42,10 @@ type Quadrant struct { intCentroid intgeom.Point } +func (q *Quadrant) Extent() geom.Extent { + return q.intExtent.ToGeomExtent() +} + // PointIndex is a pointcloud annex quadtree to enable snapping lines to a grid accounting for those points. // Quadrants: // diff --git a/tile/tile.go b/tile/tile.go index 518aba7..d550245 100644 --- a/tile/tile.go +++ b/tile/tile.go @@ -2,26 +2,22 @@ package tile import ( "github.com/go-spatial/geom" + "github.com/go-spatial/geom/encoding/mvt" "github.com/pdok/texel/pointindex" ) -func translateToTileCoord(q pointindex.Quadrant, p geom.Polygon) geom.Polygon { - translatedPol := make(geom.Polygon, len(p)) - basepoint := q.Centroid() +const precision = 4096 - for i, ring := range p.LinearRings() { - translatedPol[i] = make([][2]float64, len(ring)) - for j, point := range ring { - translatedPol[i][j] = [2]float64{point[0] - basepoint[0], point[1] - basepoint[1]} - } - } +func EncodePolygon(q pointindex.Quadrant, p geom.Polygon) ([]uint32, int32) { + ext := q.Extent() + preparedGeo := mvt.PrepareGeo(p, &ext, float64(precision)) - return p -} + // This should not be necessary. +// sg, err := convert.ToTegola(preparedGeo) +// tegolaGeo, err := validate.CleanGeometry(context.TODO(), sg, &ext) +// validatedGeo := convert.ToGeom(tegolaGeo) -func EncodePolygon(q pointindex.Quadrant, p geom.Polygon) ([]uint32, int32) { - translatedPol := translateToTileCoord(q, p) - encgeom, geomtype, err := EncodeGeometry(translatedPol) + encgeom, geomtype, err := EncodeGeometry(preparedGeo) if err != nil { panic(err) } From 90041423bade547135d1ba5d67f908e0cb3fa1da Mon Sep 17 00:00:00 2001 From: Dirk van Bree Date: Wed, 8 Jul 2026 16:36:48 +0200 Subject: [PATCH 06/27] feat: primitive detection of relevant tiles --- pointindex/pointindex.go | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/pointindex/pointindex.go b/pointindex/pointindex.go index f2b0e77..699d4df 100644 --- a/pointindex/pointindex.go +++ b/pointindex/pointindex.go @@ -77,8 +77,10 @@ type PointIndex struct { hitMultiple map[Level]map[intgeom.Point][]int } -type Level = uint -type Q = int // quadrant index (0, 1, 2 or 3) +type ( + Level = uint + Q = int // quadrant index (0, 1, 2 or 3) +) func FromTileMatrixSet(tileMatrixSet tms20.TileMatrixSet, deepestTMID tms20.TMID) (*PointIndex, error) { // assuming IsQuadTree was tested before @@ -112,6 +114,25 @@ func FromTileMatrixSet(tileMatrixSet tms20.TileMatrixSet, deepestTMID tms20.TMID return &ix, nil } +// Temporary function: primitively marks quadrants as relevant for tiling +func (ix *PointIndex) GetPrimitiveQBBox(l Level) [4]uint { + quadrants := ix.quadrants[l] + + minX := ^uint(0) + maxX := uint(0) + minY := ^uint(0) + maxY := uint(0) + + for z, _ := range quadrants { + x, y:= morton.FromZ(z) + minX = min(x, minX) + maxX = max(x, maxX) + minY = min(y, minY) + maxY = max(y, maxY) + } + return [4]uint{ minX, minY, maxX, maxY } +} + // InsertPolygon inserts all points from a Polygon func (ix *PointIndex) InsertPolygon(polygon geom.Polygon) error { // initialize the quadrants map From a949286a1fddd235420f464512258e15a553c1fb Mon Sep 17 00:00:00 2001 From: Dirk van Bree Date: Thu, 9 Jul 2026 14:18:03 +0200 Subject: [PATCH 07/27] chore: adapt to slippy api change --- tms20/tms20.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tms20/tms20.go b/tms20/tms20.go index a951f45..9c9e933 100644 --- a/tms20/tms20.go +++ b/tms20/tms20.go @@ -659,7 +659,7 @@ func (tms *TileMatrixSet) Size(zoom uint) (*slippy.Tile, bool) { if !ok { return nil, false } - return slippy.NewTile(zoom, tm.MatrixWidth, tm.MatrixHeight), true + return &slippy.Tile{Z: slippy.Zoom(zoom), X: tm.MatrixWidth, Y: tm.MatrixHeight}, true } func (tms *TileMatrixSet) FromNative(zoom uint, pt geom.Point) (*slippy.Tile, bool) { @@ -710,7 +710,7 @@ func (tms *TileMatrixSet) FromNative(zoom uint, pt geom.Point) (*slippy.Tile, bo return nil, false } - return slippy.NewTile(zoom, ux, uy), true + return &slippy.Tile{Z: slippy.Zoom(zoom), X: ux, Y: uy}, true } func (tms *TileMatrixSet) ToNative(tile *slippy.Tile) (geom.Point, bool) { From d45a6c6e7946d4825d01e996e73fbc0dd5be8fdd Mon Sep 17 00:00:00 2001 From: Dirk van Bree Date: Thu, 9 Jul 2026 14:31:53 +0200 Subject: [PATCH 08/27] feat: create tables for encoded features --- processing/gpkg/gpkg.go | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/processing/gpkg/gpkg.go b/processing/gpkg/gpkg.go index 8c4328f..00ac63c 100644 --- a/processing/gpkg/gpkg.go +++ b/processing/gpkg/gpkg.go @@ -81,7 +81,6 @@ func (source SourceGeopackage) Close() { //nolint:funlen,cyclop func (source SourceGeopackage) ReadFeatures(features chan<- processing.Feature) { - rows, err := source.handle.Query(source.Table.selectSQL()) if err != nil { log.Fatalf("err during closing rows: %s", err) @@ -174,9 +173,10 @@ func (source SourceGeopackage) GetTableInfo() []Table { } type TargetGeopackage struct { - Table Table - pagesize int - handle *gpkg.Handle + Table Table + pagesize int + handle *gpkg.Handle + encodedTable Table } func (target *TargetGeopackage) Init(file string, pagesize int) { @@ -199,6 +199,11 @@ func (target *TargetGeopackage) CreateTables(tables []Table) error { if err != nil { return err } + + err = buildEncodedTable(target.handle, table) + if err != nil { + return err + } } return nil } @@ -349,7 +354,6 @@ func getTableColumns(h *gpkg.Handle, table string) []column { var columns []column query := `PRAGMA table_info('%v');` rows, err := h.Query(fmt.Sprintf(query, table)) - if err != nil { log.Fatalf("err during closing rows: %v - %v", query, err) } @@ -392,3 +396,22 @@ func buildTable(h *gpkg.Handle, t Table) error { } return nil } + +func buildEncodedTable(h *gpkg.Handle, t Table) error { + db := h.DB + + query := fmt.Sprintf(` + CREATE TABLE IF NOT EXISTS %s ( + id INTEGER PRIMARY KEY, + tile_x INTEGER NOT NULL, + tile_y INTEGER NOT NULL, + feature_id INTEGER NOT NULL, + data BLOB + ) + `, t.Name+"_encoded") + _, err := db.Exec(query) + if err != nil { + log.Println("error adding encoded table in target GeoPackage:", err) + } + return nil +} From 3f726635460186dcabb9b1175d2cd9f50f37cdea Mon Sep 17 00:00:00 2001 From: Dirk van Bree Date: Thu, 9 Jul 2026 16:12:40 +0200 Subject: [PATCH 09/27] fix: encode arbitrary geometries --- tile/tile.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tile/tile.go b/tile/tile.go index d550245..0ab46de 100644 --- a/tile/tile.go +++ b/tile/tile.go @@ -8,9 +8,9 @@ import ( const precision = 4096 -func EncodePolygon(q pointindex.Quadrant, p geom.Polygon) ([]uint32, int32) { +func MvtEncodeGeometry(q pointindex.Quadrant, g geom.Geometry) ([]uint32, int32) { ext := q.Extent() - preparedGeo := mvt.PrepareGeo(p, &ext, float64(precision)) + preparedGeo := mvt.PrepareGeo(g, &ext, float64(precision)) // This should not be necessary. // sg, err := convert.ToTegola(preparedGeo) From 995f82bfc2153427f28b1cdd861633f3f44d7227 Mon Sep 17 00:00:00 2001 From: Dirk van Bree Date: Fri, 10 Jul 2026 08:21:00 +0200 Subject: [PATCH 10/27] fix: QBBox returns slice --- pointindex/pointindex.go | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/pointindex/pointindex.go b/pointindex/pointindex.go index 699d4df..c542164 100644 --- a/pointindex/pointindex.go +++ b/pointindex/pointindex.go @@ -115,7 +115,7 @@ func FromTileMatrixSet(tileMatrixSet tms20.TileMatrixSet, deepestTMID tms20.TMID } // Temporary function: primitively marks quadrants as relevant for tiling -func (ix *PointIndex) GetPrimitiveQBBox(l Level) [4]uint { +func (ix *PointIndex) GetPrimitiveQBBox(l Level) []Quadrant { quadrants := ix.quadrants[l] minX := ^uint(0) @@ -123,14 +123,27 @@ func (ix *PointIndex) GetPrimitiveQBBox(l Level) [4]uint { minY := ^uint(0) maxY := uint(0) - for z, _ := range quadrants { - x, y:= morton.FromZ(z) + for z := range quadrants { + x, y := morton.FromZ(z) minX = min(x, minX) maxX = max(x, maxX) minY = min(y, minY) maxY = max(y, maxY) } - return [4]uint{ minX, minY, maxX, maxY } + + quadrantSlice := make([]Quadrant, (maxY-minY)*(maxX-minX)) + for i := range maxX - minX { + for j := range maxY - minY { + extent, centroid := ix.getQuadrantExtentAndCentroid(l, minX+i, minY+j, ix.intExtent) + newQuadrant := Quadrant{ + z: morton.MustToZ(minX+i, minY+j), + intExtent: extent, + intCentroid: centroid, + } + quadrantSlice = append(quadrantSlice, newQuadrant) + } + } + return quadrantSlice } // InsertPolygon inserts all points from a Polygon From 0b60cd96a730e7120fb2bd2cc340d7b4b63cde60 Mon Sep 17 00:00:00 2001 From: Dirk van Bree Date: Fri, 10 Jul 2026 09:00:08 +0200 Subject: [PATCH 11/27] refactor: decouple geometry logic, logging and channel logic --- processing/processing.go | 160 +++++++++++++++++++++++---------------- 1 file changed, 94 insertions(+), 66 deletions(-) diff --git a/processing/processing.go b/processing/processing.go index 9a6c166..4581a01 100644 --- a/processing/processing.go +++ b/processing/processing.go @@ -18,61 +18,112 @@ func readFeaturesFromSource(source Source, features chan<- Feature) { source.ReadFeatures(features) } +func polygonsToMulti(polygons []geom.Polygon) geom.MultiPolygon { + l := len(polygons) + multiPolygon := make(geom.MultiPolygon, l) + for i := range l { + multiPolygon[i] = polygons[i] + } + return multiPolygon +} + +// processMultiPolygon will split itself into the separated polygons that will be processed before building a new MULTIPOLYGON +func processMultiPolygon(multiPolygon geom.MultiPolygon, tileMatrixIDs []tms20.TMID, f processPolygonFunc) map[tms20.TMID]geom.MultiPolygon { + newMultiPolygonPerTileMatrix := make(map[tms20.TMID]geom.MultiPolygon, len(tileMatrixIDs)) + for _, polygon := range multiPolygon { + newPolygonsPerTileMatrix := f(polygon, tileMatrixIDs) + for tmID, newPolygons := range newPolygonsPerTileMatrix { + for _, newPolygon := range newPolygons { + // if the processing results in multiple polygons, they are just added to the single resulting multipoly + newMultiPolygonPerTileMatrix[tmID] = append(newMultiPolygonPerTileMatrix[tmID], newPolygon) + } + } + } + return newMultiPolygonPerTileMatrix +} + +func processGeometry(geometry geom.Geometry, tmIDs []tms20.TMID, f processPolygonFunc) map[tms20.TMID]geom.Geometry { + newGeometriesPerTileMatrix := make(map[tms20.TMID]geom.Geometry, len(tmIDs)) + + switch geometry := geometry.(type) { + case geom.Polygon: + newPolygonsPerTileMatrix := f(geometry, tmIDs) + for tmID, newPolygons := range newPolygonsPerTileMatrix { + if len(newPolygons) == 0 { // should never happen + panic(fmt.Errorf("no new polygon for level %v", tmID)) + } + if len(newPolygons) == 1 { + newGeometriesPerTileMatrix[tmID] = newPolygons[0] + } else { + // TODO polygons are combined into multipolygons, for now here + // later, processPolygonFunc could return abstract geometry(s) if also lines/points are returned + newGeometriesPerTileMatrix[tmID] = polygonsToMulti(newPolygons) + } + } + case geom.MultiPolygon: + newMultiPolygonPerTileMatrix := processMultiPolygon(geometry, tmIDs, f) + for tmID, newMultiPolygon := range newMultiPolygonPerTileMatrix { + newGeometriesPerTileMatrix[tmID] = newMultiPolygon + } + default: + for _, tmID := range tmIDs { + newGeometriesPerTileMatrix[tmID] = nil + } + } + + return newGeometriesPerTileMatrix +} + +type countStats struct { + preCount uint64 + postCount uint64 + nonPolygonCount uint64 + multiPolygonCount uint64 +} + +func initStats() countStats { + return countStats{0, 0, 0, 0} +} + +func (stats *countStats) countGeometry(g geom.Geometry) { + switch g.(type) { + case geom.MultiPolygon: + stats.nonPolygonCount++ + case geom.Polygon: + default: + stats.nonPolygonCount++ + } +} + // processFeatures processes the geometries in the features with the given function func processFeatures(featuresIn <-chan Feature, featuresOut chan<- FeatureForTileMatrix, tmIDs []tms20.TMID, f processPolygonFunc) { - var preCount, postCount, nonPolygonCount, multiPolygonCount uint64 + stats := initStats() for { feature, hasMore := <-featuresIn if !hasMore { break } - preCount++ - switch feature.Geometry().(type) { - case geom.Polygon: - polygon := feature.Geometry().(geom.Polygon) - newPolygonsPerTileMatrix := f(polygon, tmIDs) - if len(newPolygonsPerTileMatrix) > 0 { - postCount++ - } - for tmID, newPolygons := range newPolygonsPerTileMatrix { - var newGeometry geom.Geometry - if len(newPolygons) == 0 { // should never happen - panic(fmt.Errorf("no new polygon for level %v", tmID)) - } - if len(newPolygons) == 1 { - newGeometry = newPolygons[0] - } else { - // TODO polygons are combined into multipolygons, for now here - // later, processPolygonFunc could return abstract geometry(s) if also lines/points are returned - newGeometry = polygonsToMulti(newPolygons) - } - featuresOut <- wrapFeatureForTileMatrix(feature, tmID, newGeometry) - } - case geom.MultiPolygon: - multiPolygon := feature.Geometry().(geom.MultiPolygon) - newMultiPolygonPerTileMatrix := processMultiPolygon(multiPolygon, tmIDs, f) - if len(newMultiPolygonPerTileMatrix) > 0 { - postCount++ - } - for tmID, newMultiPolygon := range newMultiPolygonPerTileMatrix { - featuresOut <- wrapFeatureForTileMatrix(feature, tmID, newMultiPolygon) - } - default: - postCount++ - nonPolygonCount++ - for _, tmID := range tmIDs { - featuresOut <- wrapFeatureForTileMatrix(feature, tmID, nil) - } + stats.preCount++ + geometry := feature.Geometry() + stats.countGeometry(geometry) + newGeometriesPerTileMatrix := processGeometry(feature.Geometry(), tmIDs, f) + + if len(newGeometriesPerTileMatrix) > 0 { + stats.postCount++ + } + for tmID, newGeom := range newGeometriesPerTileMatrix { + featuresOut <- wrapFeatureForTileMatrix(feature, tmID, newGeom) } + } close(featuresOut) - log.Printf(" total features: %d", preCount) - log.Printf(" non-polygons: %d", nonPolygonCount) - if preCount != nonPolygonCount { - log.Printf(" multipolygons: %d", multiPolygonCount) + log.Printf(" total features: %d", stats.preCount) + log.Printf(" non-polygons: %d", stats.nonPolygonCount) + if stats.preCount != stats.nonPolygonCount { + log.Printf(" multipolygons: %d", stats.multiPolygonCount) } - log.Printf(" kept: %d", postCount) + log.Printf(" kept: %d", stats.postCount) } // writeFeatures collects the processed features by the processFeatures and @@ -115,21 +166,6 @@ func writeFeaturesToTargets(featuresForTileMatrices <-chan FeatureForTileMatrix, wg.Wait() } -// processMultiPolygon will split itself into the separated polygons that will be processed before building a new MULTIPOLYGON -func processMultiPolygon(multiPolygon geom.MultiPolygon, tileMatrixIDs []tms20.TMID, f processPolygonFunc) map[tms20.TMID]geom.MultiPolygon { - newMultiPolygonPerTileMatrix := make(map[tms20.TMID]geom.MultiPolygon, len(tileMatrixIDs)) - for _, polygon := range multiPolygon { - newPolygonsPerTileMatrix := f(polygon, tileMatrixIDs) - for tmID, newPolygons := range newPolygonsPerTileMatrix { - for _, newPolygon := range newPolygons { - // if the processing results in multiple polygons, they are just added to the single resulting multipoly - newMultiPolygonPerTileMatrix[tmID] = append(newMultiPolygonPerTileMatrix[tmID], newPolygon) - } - } - } - return newMultiPolygonPerTileMatrix -} - type processPolygonFunc func(p geom.Polygon, tileMatrixIDs []tms20.TMID) map[tms20.TMID][]geom.Polygon // ProcessFeatures applies the processing function/operation to each Target. @@ -182,11 +218,3 @@ func wrapFeatureForTileMatrix(feature Feature, tileMatrixID int, newGeometry geo } } -func polygonsToMulti(polygons []geom.Polygon) geom.MultiPolygon { - l := len(polygons) - multiPolygon := make(geom.MultiPolygon, l) - for i := range l { - multiPolygon[i] = polygons[i] - } - return multiPolygon -} From fd637647c6ad47bbd09cd8c259e3eec22720488f Mon Sep 17 00:00:00 2001 From: Dirk van Bree Date: Fri, 10 Jul 2026 10:44:22 +0200 Subject: [PATCH 12/27] feat: snap meta-results available during processing --- main.go | 2 +- processing/interface.go | 6 +++ processing/processing.go | 99 ++++++++++++++++++++++++++-------------- snap/snap.go | 11 +++-- 4 files changed, 80 insertions(+), 38 deletions(-) diff --git a/main.go b/main.go index 6b5c732..dbd5495 100644 --- a/main.go +++ b/main.go @@ -223,7 +223,7 @@ func injectSuffixIntoPath(p string) string { } func processBySnapping(source processing.Source, targets map[tms20.TMID]processing.Target, tileMatrixSet tms20.TileMatrixSet, snapConfig snap.Config) { - processing.ProcessFeatures(source, targets, func(p geom.Polygon, tmIDs []tms20.TMID) map[tms20.TMID][]geom.Polygon { + processing.ProcessFeatures(source, targets, func(p geom.Polygon, tmIDs []tms20.TMID) map[tms20.TMID]processing.SnapResult { return snap.SnapPolygon(p, tileMatrixSet, tmIDs, snapConfig) }) } diff --git a/processing/interface.go b/processing/interface.go index 1aa20ab..2928348 100644 --- a/processing/interface.go +++ b/processing/interface.go @@ -2,6 +2,7 @@ package processing import ( "github.com/go-spatial/geom" + "github.com/pdok/texel/pointindex" ) type Feature interface { @@ -14,6 +15,11 @@ type FeatureForTileMatrix interface { TileMatrixID() int } +type SnapResult struct { + Geometry geom.Geometry + Tiles []pointindex.Quadrant +} + type Source interface { ReadFeatures(ch chan<- Feature) } diff --git a/processing/processing.go b/processing/processing.go index 4581a01..6203fd2 100644 --- a/processing/processing.go +++ b/processing/processing.go @@ -18,7 +18,21 @@ func readFeaturesFromSource(source Source, features chan<- Feature) { source.ReadFeatures(features) } -func polygonsToMulti(polygons []geom.Polygon) geom.MultiPolygon { +func PolygonSliceToGeom(polygons []geom.Polygon) geom.Geometry { + if len(polygons) == 0 { + panic("Multipolygon with zero polygons encountered") + } + if len(polygons) == 1 { + return polygons[0] + } + multipolygon := make(geom.MultiPolygon, len(polygons)) + for i, p := range polygons { + multipolygon[i] = p + } + return multipolygon +} + +func PolygonsToMulti(polygons []geom.Polygon) geom.MultiPolygon { l := len(polygons) multiPolygon := make(geom.MultiPolygon, l) for i := range l { @@ -27,47 +41,67 @@ func polygonsToMulti(polygons []geom.Polygon) geom.MultiPolygon { return multiPolygon } +func mergeGeometries(g, h geom.Geometry) geom.Geometry { + if g == nil { + return h + } + if h == nil { + return g + } + + switch g := g.(type) { + case geom.Polygon: + switch h := h.(type) { + case geom.Polygon: + return geom.MultiPolygon{g, h} + case geom.MultiPolygon: + return append(h, g) + default: + panic("Trying to merge a non-polygon geometry.") + + } + case geom.MultiPolygon: + switch h := h.(type) { + case geom.Polygon: + return append(g, h) + case geom.MultiPolygon: + return append(g, h.Polygons()...) + default: + panic("Trying to merge a non-polygon geometry.") + } + + default: + panic("Trying to merge a non-polygon geometry.") + } +} + // processMultiPolygon will split itself into the separated polygons that will be processed before building a new MULTIPOLYGON -func processMultiPolygon(multiPolygon geom.MultiPolygon, tileMatrixIDs []tms20.TMID, f processPolygonFunc) map[tms20.TMID]geom.MultiPolygon { - newMultiPolygonPerTileMatrix := make(map[tms20.TMID]geom.MultiPolygon, len(tileMatrixIDs)) +func processMultiPolygon(multiPolygon geom.MultiPolygon, tileMatrixIDs []tms20.TMID, f processPolygonFunc) map[tms20.TMID]SnapResult { + newMultiPolygonPerTileMatrix := make(map[tms20.TMID]SnapResult, len(tileMatrixIDs)) for _, polygon := range multiPolygon { - newPolygonsPerTileMatrix := f(polygon, tileMatrixIDs) - for tmID, newPolygons := range newPolygonsPerTileMatrix { - for _, newPolygon := range newPolygons { - // if the processing results in multiple polygons, they are just added to the single resulting multipoly - newMultiPolygonPerTileMatrix[tmID] = append(newMultiPolygonPerTileMatrix[tmID], newPolygon) - } + snapResultsPerTileMatrix := f(polygon, tileMatrixIDs) + for tmID, snapResult := range snapResultsPerTileMatrix { + currentResult := newMultiPolygonPerTileMatrix[tmID] + currentResult.Tiles = append(currentResult.Tiles, snapResult.Tiles...) + currentResult.Geometry = mergeGeometries(currentResult.Geometry, snapResult.Geometry) + newMultiPolygonPerTileMatrix[tmID] = currentResult + } } return newMultiPolygonPerTileMatrix } -func processGeometry(geometry geom.Geometry, tmIDs []tms20.TMID, f processPolygonFunc) map[tms20.TMID]geom.Geometry { - newGeometriesPerTileMatrix := make(map[tms20.TMID]geom.Geometry, len(tmIDs)) +func processGeometry(geometry geom.Geometry, tmIDs []tms20.TMID, f processPolygonFunc) map[tms20.TMID]SnapResult { + newGeometriesPerTileMatrix := make(map[tms20.TMID]SnapResult, len(tmIDs)) switch geometry := geometry.(type) { case geom.Polygon: - newPolygonsPerTileMatrix := f(geometry, tmIDs) - for tmID, newPolygons := range newPolygonsPerTileMatrix { - if len(newPolygons) == 0 { // should never happen - panic(fmt.Errorf("no new polygon for level %v", tmID)) - } - if len(newPolygons) == 1 { - newGeometriesPerTileMatrix[tmID] = newPolygons[0] - } else { - // TODO polygons are combined into multipolygons, for now here - // later, processPolygonFunc could return abstract geometry(s) if also lines/points are returned - newGeometriesPerTileMatrix[tmID] = polygonsToMulti(newPolygons) - } - } + newGeometriesPerTileMatrix = f(geometry, tmIDs) case geom.MultiPolygon: - newMultiPolygonPerTileMatrix := processMultiPolygon(geometry, tmIDs, f) - for tmID, newMultiPolygon := range newMultiPolygonPerTileMatrix { - newGeometriesPerTileMatrix[tmID] = newMultiPolygon - } + newGeometriesPerTileMatrix = processMultiPolygon(geometry, tmIDs, f) default: for _, tmID := range tmIDs { - newGeometriesPerTileMatrix[tmID] = nil + newGeometriesPerTileMatrix[tmID] = SnapResult{nil, nil} } } @@ -111,8 +145,8 @@ func processFeatures(featuresIn <-chan Feature, featuresOut chan<- FeatureForTil if len(newGeometriesPerTileMatrix) > 0 { stats.postCount++ } - for tmID, newGeom := range newGeometriesPerTileMatrix { - featuresOut <- wrapFeatureForTileMatrix(feature, tmID, newGeom) + for tmID, snapResult := range newGeometriesPerTileMatrix { + featuresOut <- wrapFeatureForTileMatrix(feature, tmID, snapResult.Geometry) } } @@ -166,7 +200,7 @@ func writeFeaturesToTargets(featuresForTileMatrices <-chan FeatureForTileMatrix, wg.Wait() } -type processPolygonFunc func(p geom.Polygon, tileMatrixIDs []tms20.TMID) map[tms20.TMID][]geom.Polygon +type processPolygonFunc func(p geom.Polygon, tileMatrixIDs []tms20.TMID) map[tms20.TMID]SnapResult // ProcessFeatures applies the processing function/operation to each Target. func ProcessFeatures(source Source, targets map[tms20.TMID]Target, f processPolygonFunc) { @@ -217,4 +251,3 @@ func wrapFeatureForTileMatrix(feature Feature, tileMatrixID int, newGeometry geo tileMatrixID: tileMatrixID, } } - diff --git a/snap/snap.go b/snap/snap.go index f3bf968..a849fd6 100644 --- a/snap/snap.go +++ b/snap/snap.go @@ -18,6 +18,7 @@ import ( "github.com/go-spatial/geom" "github.com/pdok/texel/intgeom" + "github.com/pdok/texel/processing" "github.com/pdok/texel/tms20" orderedmap "github.com/wk8/go-ordered-map/v2" ) @@ -39,7 +40,7 @@ type Config struct { // and adds points to lines to prevent intersections. // //nolint:revive -func SnapPolygon(polygon geom.Polygon, tileMatrixSet tms20.TileMatrixSet, tmIDs []tms20.TMID, config Config) map[tms20.TMID][]geom.Polygon { +func SnapPolygon(polygon geom.Polygon, tileMatrixSet tms20.TileMatrixSet, tmIDs []tms20.TMID, config Config) map[tms20.TMID]processing.SnapResult { deepestID := slices.Max(tmIDs) ix, err := pointindex.FromTileMatrixSet(tileMatrixSet, deepestID) if err != nil { @@ -56,7 +57,7 @@ func SnapPolygon(polygon geom.Polygon, tileMatrixSet tms20.TileMatrixSet, tmIDs outsideGridErr := new(pointindex.OutsideGridError) if errors.As(err, outsideGridErr) && config.IgnoreOutsideGrid { log.Println("[WARNING] skipping polygon because: " + err.Error()) - return make(map[tms20.TMID][]geom.Polygon) + return make(map[tms20.TMID]processing.SnapResult) } else { panic(err) } @@ -64,9 +65,11 @@ func SnapPolygon(polygon geom.Polygon, tileMatrixSet tms20.TileMatrixSet, tmIDs newPolygonsPerLevel := addPointsAndSnap(ix, polygon, levels, config) - newPolygonsPerTileMatrixID := make(map[tms20.TMID][]geom.Polygon, len(newPolygonsPerLevel)) + newPolygonsPerTileMatrixID := make(map[tms20.TMID]processing.SnapResult, len(newPolygonsPerLevel)) for level, newPolygons := range newPolygonsPerLevel { - newPolygonsPerTileMatrixID[tmIDsByLevels[level]] = newPolygons + tilesbbox := ix.GetPrimitiveQBBox(pointindex.Level(tmIDsByLevels[level])) + newGeometry := processing.PolygonSliceToGeom(newPolygons) + newPolygonsPerTileMatrixID[tmIDsByLevels[level]] = processing.SnapResult{Geometry: newGeometry, Tiles: tilesbbox} } return newPolygonsPerTileMatrixID From c2a67cfea438357da17527081c482414220d8a02 Mon Sep 17 00:00:00 2001 From: Dirk van Bree Date: Fri, 10 Jul 2026 11:04:41 +0200 Subject: [PATCH 13/27] fix: create geometry_type field in encodedtable --- processing/gpkg/gpkg.go | 1 + 1 file changed, 1 insertion(+) diff --git a/processing/gpkg/gpkg.go b/processing/gpkg/gpkg.go index 00ac63c..fd385c0 100644 --- a/processing/gpkg/gpkg.go +++ b/processing/gpkg/gpkg.go @@ -406,6 +406,7 @@ func buildEncodedTable(h *gpkg.Handle, t Table) error { tile_x INTEGER NOT NULL, tile_y INTEGER NOT NULL, feature_id INTEGER NOT NULL, + geometry_type INTEGER NOT NULL data BLOB ) `, t.Name+"_encoded") From ac8917d033a20addc0370bf961c3a67db2a3a68b Mon Sep 17 00:00:00 2001 From: Dirk van Bree Date: Mon, 13 Jul 2026 10:22:20 +0200 Subject: [PATCH 14/27] feat: types for encoded data --- pointindex/pointindex.go | 4 ++++ processing/interface.go | 14 +++++++++++++- tile/tile.go | 13 +++++++++++-- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/pointindex/pointindex.go b/pointindex/pointindex.go index c542164..3c0c683 100644 --- a/pointindex/pointindex.go +++ b/pointindex/pointindex.go @@ -46,6 +46,10 @@ func (q *Quadrant) Extent() geom.Extent { return q.intExtent.ToGeomExtent() } +func (q *Quadrant) Coords() (uint, uint) { + return morton.FromZ(q.z) +} + // PointIndex is a pointcloud annex quadtree to enable snapping lines to a grid accounting for those points. // Quadrants: // diff --git a/processing/interface.go b/processing/interface.go index 2928348..77d0257 100644 --- a/processing/interface.go +++ b/processing/interface.go @@ -17,7 +17,19 @@ type FeatureForTileMatrix interface { type SnapResult struct { Geometry geom.Geometry - Tiles []pointindex.Quadrant + Tiles []pointindex.Quadrant +} + +type EncodedGeometry struct { + Encoding []uint32 + GeometryType int32 + XTile uint + YTile uint +} + +type EncodedFeature struct { + Feature Feature + EncodedGeoms []EncodedGeometry } type Source interface { diff --git a/tile/tile.go b/tile/tile.go index 0ab46de..576a326 100644 --- a/tile/tile.go +++ b/tile/tile.go @@ -4,11 +4,12 @@ import ( "github.com/go-spatial/geom" "github.com/go-spatial/geom/encoding/mvt" "github.com/pdok/texel/pointindex" + "github.com/pdok/texel/processing" ) const precision = 4096 -func MvtEncodeGeometry(q pointindex.Quadrant, g geom.Geometry) ([]uint32, int32) { +func MvtEncodeGeometry(q pointindex.Quadrant, g geom.Geometry) processing.EncodedGeometry { ext := q.Extent() preparedGeo := mvt.PrepareGeo(g, &ext, float64(precision)) @@ -21,6 +22,14 @@ func MvtEncodeGeometry(q pointindex.Quadrant, g geom.Geometry) ([]uint32, int32) if err != nil { panic(err) } + - return encgeom, int32(geomtype) + xTile, yTile := q.Coords() + + return processing.EncodedGeometry{ + Encoding: encgeom, + GeometryType: int32(geomtype), + XTile: xTile, + YTile: yTile, + } } From 13c1944d86605a1a57b96c4085549a597f6732a3 Mon Sep 17 00:00:00 2001 From: Dirk van Bree Date: Mon, 13 Jul 2026 10:56:58 +0200 Subject: [PATCH 15/27] feat: encoded geometries to database layer --- processing/gpkg/gpkg.go | 2 +- processing/interface.go | 13 ++++--------- processing/processing.go | 27 +++++++++++++++++++++++---- tile/tile.go | 12 +++++++++--- 4 files changed, 37 insertions(+), 17 deletions(-) diff --git a/processing/gpkg/gpkg.go b/processing/gpkg/gpkg.go index fd385c0..bc846e3 100644 --- a/processing/gpkg/gpkg.go +++ b/processing/gpkg/gpkg.go @@ -208,7 +208,7 @@ func (target *TargetGeopackage) CreateTables(tables []Table) error { return nil } -func (target *TargetGeopackage) WriteFeatures(inFeatures <-chan processing.Feature) { +func (target *TargetGeopackage) WriteFeatures(inFeatures <-chan processing.FeatureForTileMatrix) { var features []processing.Feature for { diff --git a/processing/interface.go b/processing/interface.go index 77d0257..04d9a5c 100644 --- a/processing/interface.go +++ b/processing/interface.go @@ -3,6 +3,7 @@ package processing import ( "github.com/go-spatial/geom" "github.com/pdok/texel/pointindex" + "github.com/pdok/texel/tile" ) type Feature interface { @@ -13,6 +14,7 @@ type Feature interface { type FeatureForTileMatrix interface { Feature TileMatrixID() int + EncodedGeoms() []tile.EncodedGeometry } type SnapResult struct { @@ -20,16 +22,9 @@ type SnapResult struct { Tiles []pointindex.Quadrant } -type EncodedGeometry struct { - Encoding []uint32 - GeometryType int32 - XTile uint - YTile uint -} - type EncodedFeature struct { Feature Feature - EncodedGeoms []EncodedGeometry + EncodedGeoms []tile.EncodedGeometry } type Source interface { @@ -37,5 +32,5 @@ type Source interface { } type Target interface { - WriteFeatures(ch <-chan Feature) + WriteFeatures(ch <-chan FeatureForTileMatrix) } diff --git a/processing/processing.go b/processing/processing.go index 6203fd2..9548f0e 100644 --- a/processing/processing.go +++ b/processing/processing.go @@ -7,6 +7,7 @@ import ( "log" "sync" + "github.com/pdok/texel/tile" "github.com/pdok/texel/tms20" "github.com/go-spatial/geom" @@ -129,6 +130,17 @@ func (stats *countStats) countGeometry(g geom.Geometry) { } } +func encodeGeometry(s SnapResult) []tile.EncodedGeometry { + encGeoms := make([]tile.EncodedGeometry, len(s.Tiles)) + orig := s.Geometry + + for i, q := range s.Tiles { + encGeoms[i] = tile.MvtEncodeGeometry(q, orig) + } + + return encGeoms +} + // processFeatures processes the geometries in the features with the given function func processFeatures(featuresIn <-chan Feature, featuresOut chan<- FeatureForTileMatrix, tmIDs []tms20.TMID, f processPolygonFunc) { stats := initStats() @@ -146,7 +158,8 @@ func processFeatures(featuresIn <-chan Feature, featuresOut chan<- FeatureForTil stats.postCount++ } for tmID, snapResult := range newGeometriesPerTileMatrix { - featuresOut <- wrapFeatureForTileMatrix(feature, tmID, snapResult.Geometry) + encGeoms := encodeGeometry(snapResult) + featuresOut <- wrapFeatureForTileMatrix(feature, tmID, snapResult.Geometry, encGeoms) } } @@ -164,12 +177,12 @@ func processFeatures(featuresIn <-chan Feature, featuresOut chan<- FeatureForTil // creates a WKB binary from the geometry // The collected feature array, based on the pagesize, is then passed to the writeFeaturesArray func writeFeaturesToTargets(featuresForTileMatrices <-chan FeatureForTileMatrix, targets map[int]Target) { - targetChannels := make(map[int]chan<- Feature) + targetChannels := make(map[int]chan<- FeatureForTileMatrix) wg := sync.WaitGroup{} // create a channel and start a goroutine per tile matrix target for tmID, target := range targets { - targetChannel := make(chan Feature) + targetChannel := make(chan FeatureForTileMatrix) targetChannels[tmID] = targetChannel wg.Add(1) go func(target Target) { @@ -226,6 +239,7 @@ func ProcessFeatures(source Source, targets map[tms20.TMID]Target, f processPoly type featureForTileMatrixWrapper struct { wrapped Feature newGeometry geom.Geometry + encodedGeoms []tile.EncodedGeometry tileMatrixID int } @@ -244,10 +258,15 @@ func (f *featureForTileMatrixWrapper) TileMatrixID() int { return f.tileMatrixID } -func wrapFeatureForTileMatrix(feature Feature, tileMatrixID int, newGeometry geom.Geometry) FeatureForTileMatrix { +func (f *featureForTileMatrixWrapper) EncodedGeoms() []tile.EncodedGeometry { + return f.encodedGeoms +} + +func wrapFeatureForTileMatrix(feature Feature, tileMatrixID int, newGeometry geom.Geometry, encGeoms []tile.EncodedGeometry) FeatureForTileMatrix { return &featureForTileMatrixWrapper{ wrapped: feature, newGeometry: newGeometry, tileMatrixID: tileMatrixID, + encodedGeoms: encGeoms, } } diff --git a/tile/tile.go b/tile/tile.go index 576a326..0ef671d 100644 --- a/tile/tile.go +++ b/tile/tile.go @@ -4,12 +4,18 @@ import ( "github.com/go-spatial/geom" "github.com/go-spatial/geom/encoding/mvt" "github.com/pdok/texel/pointindex" - "github.com/pdok/texel/processing" ) const precision = 4096 -func MvtEncodeGeometry(q pointindex.Quadrant, g geom.Geometry) processing.EncodedGeometry { +type EncodedGeometry struct { + Encoding []uint32 + GeometryType int32 + XTile uint + YTile uint +} + +func MvtEncodeGeometry(q pointindex.Quadrant, g geom.Geometry) EncodedGeometry { ext := q.Extent() preparedGeo := mvt.PrepareGeo(g, &ext, float64(precision)) @@ -26,7 +32,7 @@ func MvtEncodeGeometry(q pointindex.Quadrant, g geom.Geometry) processing.Encode xTile, yTile := q.Coords() - return processing.EncodedGeometry{ + return EncodedGeometry{ Encoding: encgeom, GeometryType: int32(geomtype), XTile: xTile, From b462c2369c8d0a51251940ded69d673aaf4834fc Mon Sep 17 00:00:00 2001 From: Dirk van Bree Date: Mon, 13 Jul 2026 13:44:22 +0200 Subject: [PATCH 16/27] feat: write encoded geometries to target geopackage --- processing/gpkg/encoded.go | 70 ++++++++++++++++++++++++++++++++++++++ processing/gpkg/gpkg.go | 33 ++++-------------- 2 files changed, 77 insertions(+), 26 deletions(-) create mode 100644 processing/gpkg/encoded.go diff --git a/processing/gpkg/encoded.go b/processing/gpkg/encoded.go new file mode 100644 index 0000000..8e96eb3 --- /dev/null +++ b/processing/gpkg/encoded.go @@ -0,0 +1,70 @@ +package gpkg + +import ( + "fmt" + "log" + + "github.com/go-spatial/geom/encoding/gpkg" + "github.com/pdok/texel/processing" +) + +func (t Table) EncodedName() string { + return t.Name + "_encoded" +} + +func (t Table) insertSQLEncoded() string { + return `INSERT INTO "` + t.EncodedName() + + `(tile_x, tile_y, feature_id, geometry_type, data)` + + ` VALUES (?, ?, ?, ?, ?)` +} + +func (target *TargetGeopackage) writeEncodedFeatures(encFeature []processing.FeatureForTileMatrix) { + tx, err := target.handle.Begin() + if err != nil { + log.Fatalf("Could not start a transaction: %s", err) + } + + stmt, err := tx.Prepare(target.Table.insertSQLEncoded()) + if err != nil { + log.Fatalf("Could not prepare a statement: %s", err) + } + + for _, ef := range encFeature { + cols := ef.Columns() + if len(cols) == 0 { + log.Fatalf("Processing feature without data") + } + fid := cols[0] + for _, eg := range ef.EncodedGeoms() { + _, err := stmt.Exec(eg.XTile, eg.YTile, fid, eg.GeometryType, eg.Encoding) + if err != nil { + log.Fatalf("Could not get a result summary from the prepared statement for fid %s: %s", fid, err) + } + } + + } + + stmt.Close() + _ = tx.Commit() +} + +func buildEncodedTable(h *gpkg.Handle, t Table) error { + db := h.DB + + query := fmt.Sprintf(` + CREATE TABLE IF NOT EXISTS %s ( + id INTEGER PRIMARY KEY, + tile_x INTEGER NOT NULL, + tile_y INTEGER NOT NULL, + feature_id INTEGER NOT NULL, + geometry_type INTEGER NOT NULL + data BLOB + ) + `, t.EncodedName()) + _, err := db.Exec(query) + if err != nil { + log.Println("error adding encoded table in target GeoPackage:", err) + return err + } + return nil +} diff --git a/processing/gpkg/gpkg.go b/processing/gpkg/gpkg.go index bc846e3..ed335e0 100644 --- a/processing/gpkg/gpkg.go +++ b/processing/gpkg/gpkg.go @@ -173,10 +173,9 @@ func (source SourceGeopackage) GetTableInfo() []Table { } type TargetGeopackage struct { - Table Table - pagesize int - handle *gpkg.Handle - encodedTable Table + Table Table + pagesize int + handle *gpkg.Handle } func (target *TargetGeopackage) Init(file string, pagesize int) { @@ -209,24 +208,26 @@ func (target *TargetGeopackage) CreateTables(tables []Table) error { } func (target *TargetGeopackage) WriteFeatures(inFeatures <-chan processing.FeatureForTileMatrix) { - var features []processing.Feature + var features []processing.FeatureForTileMatrix for { feature, hasMore := <-inFeatures if !hasMore { target.writeFeatures(features) + target.writeEncodedFeatures(features) break } features = append(features, feature) if len(features)%target.pagesize == 0 { target.writeFeatures(features) + target.writeEncodedFeatures(features) features = nil } } } -func (target *TargetGeopackage) writeFeatures(features []processing.Feature) { +func (target *TargetGeopackage) writeFeatures(features []processing.FeatureForTileMatrix) { tx, err := target.handle.Begin() if err != nil { log.Fatalf("Could not start a transaction: %s", err) @@ -396,23 +397,3 @@ func buildTable(h *gpkg.Handle, t Table) error { } return nil } - -func buildEncodedTable(h *gpkg.Handle, t Table) error { - db := h.DB - - query := fmt.Sprintf(` - CREATE TABLE IF NOT EXISTS %s ( - id INTEGER PRIMARY KEY, - tile_x INTEGER NOT NULL, - tile_y INTEGER NOT NULL, - feature_id INTEGER NOT NULL, - geometry_type INTEGER NOT NULL - data BLOB - ) - `, t.Name+"_encoded") - _, err := db.Exec(query) - if err != nil { - log.Println("error adding encoded table in target GeoPackage:", err) - } - return nil -} From b1057e8468d2343ee29480172ad122905b6aa62b Mon Sep 17 00:00:00 2001 From: Dirk van Bree Date: Mon, 13 Jul 2026 13:46:24 +0200 Subject: [PATCH 17/27] fix: comma in SQL statement --- processing/gpkg/encoded.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/processing/gpkg/encoded.go b/processing/gpkg/encoded.go index 8e96eb3..8966ef5 100644 --- a/processing/gpkg/encoded.go +++ b/processing/gpkg/encoded.go @@ -57,7 +57,7 @@ func buildEncodedTable(h *gpkg.Handle, t Table) error { tile_x INTEGER NOT NULL, tile_y INTEGER NOT NULL, feature_id INTEGER NOT NULL, - geometry_type INTEGER NOT NULL + geometry_type INTEGER NOT NULL, data BLOB ) `, t.EncodedName()) From f4e807bb9b29abdda837846ed4866b9fb333f5f8 Mon Sep 17 00:00:00 2001 From: Dirk van Bree Date: Mon, 13 Jul 2026 14:08:32 +0200 Subject: [PATCH 18/27] fix: wrong mix of initialize and append to slice --- pointindex/pointindex.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pointindex/pointindex.go b/pointindex/pointindex.go index 3c0c683..d06e903 100644 --- a/pointindex/pointindex.go +++ b/pointindex/pointindex.go @@ -144,7 +144,7 @@ func (ix *PointIndex) GetPrimitiveQBBox(l Level) []Quadrant { intExtent: extent, intCentroid: centroid, } - quadrantSlice = append(quadrantSlice, newQuadrant) + quadrantSlice[i * (maxY - minY) + j] = newQuadrant } } return quadrantSlice From ad77fc4d3293e20dd880f73b307874681a8a0a37 Mon Sep 17 00:00:00 2001 From: Dirk van Bree Date: Mon, 13 Jul 2026 14:10:33 +0200 Subject: [PATCH 19/27] fix: SQL statement syntax error --- processing/gpkg/encoded.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/processing/gpkg/encoded.go b/processing/gpkg/encoded.go index 8966ef5..91cdbb9 100644 --- a/processing/gpkg/encoded.go +++ b/processing/gpkg/encoded.go @@ -13,8 +13,8 @@ func (t Table) EncodedName() string { } func (t Table) insertSQLEncoded() string { - return `INSERT INTO "` + t.EncodedName() + - `(tile_x, tile_y, feature_id, geometry_type, data)` + + return `INSERT INTO "` + t.EncodedName() + `"` + + ` (tile_x, tile_y, feature_id, geometry_type, data)` + ` VALUES (?, ?, ?, ?, ?)` } From 8b52d84a099ce12be12034a4f05adfabe67df95d Mon Sep 17 00:00:00 2001 From: Dirk van Bree Date: Tue, 14 Jul 2026 10:16:41 +0200 Subject: [PATCH 20/27] fix: serialize int slice for storage --- processing/gpkg/encoded.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/processing/gpkg/encoded.go b/processing/gpkg/encoded.go index 91cdbb9..a6b410b 100644 --- a/processing/gpkg/encoded.go +++ b/processing/gpkg/encoded.go @@ -1,6 +1,7 @@ package gpkg import ( + "encoding/binary" "fmt" "log" @@ -13,11 +14,21 @@ func (t Table) EncodedName() string { } func (t Table) insertSQLEncoded() string { - return `INSERT INTO "` + t.EncodedName() + `"` + + return `INSERT INTO "` + t.EncodedName() + `"` + ` (tile_x, tile_y, feature_id, geometry_type, data)` + ` VALUES (?, ?, ?, ?, ?)` } +func serializeToBytes(intslice []uint32) []byte { + bytes := make([]byte, len(intslice)*4) + + for i, x := range intslice { + binary.LittleEndian.PutUint32(bytes[i*4:], x) + } + + return bytes +} + func (target *TargetGeopackage) writeEncodedFeatures(encFeature []processing.FeatureForTileMatrix) { tx, err := target.handle.Begin() if err != nil { @@ -36,7 +47,8 @@ func (target *TargetGeopackage) writeEncodedFeatures(encFeature []processing.Fea } fid := cols[0] for _, eg := range ef.EncodedGeoms() { - _, err := stmt.Exec(eg.XTile, eg.YTile, fid, eg.GeometryType, eg.Encoding) + bytes := serializeToBytes(eg.Encoding) + _, err := stmt.Exec(eg.XTile, eg.YTile, fid, eg.GeometryType, bytes) if err != nil { log.Fatalf("Could not get a result summary from the prepared statement for fid %s: %s", fid, err) } From e9f1d6165fad59f69bb56a13f302e78c0876abdc Mon Sep 17 00:00:00 2001 From: Dirk van Bree Date: Tue, 14 Jul 2026 10:41:20 +0200 Subject: [PATCH 21/27] fix: update snap test with new snap return construct --- snap/snap_test.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/snap/snap_test.go b/snap/snap_test.go index 6f2fb8a..196c6a3 100644 --- a/snap/snap_test.go +++ b/snap/snap_test.go @@ -7,6 +7,7 @@ import ( "github.com/pdok/texel/geomhelp" "github.com/pdok/texel/mathhelp" "github.com/pdok/texel/pointindex" + "github.com/pdok/texel/processing" "github.com/stretchr/testify/require" "github.com/go-spatial/geom/encoding/wkt" @@ -790,9 +791,10 @@ func TestSnap_snapPolygon(t *testing.T) { } got := SnapPolygon(tt.polygon, tt.tms, tt.tmIDs, tt.config) for tmID, wantPoly := range tt.want { - if !assert.Equal(t, wantPoly, got[tmID]) { + wantGeom := processing.PolygonSliceToGeom(wantPoly) + if !assert.Equal(t, wantGeom, got[tmID].Geometry) { t.Errorf("snapPolygon(%v, _, %v)\n= %v\nwant: %v", - wkt.MustEncode(tt.polygon), tmID, geomhelp.WktMustEncodeSlice(got[tmID], 0), geomhelp.WktMustEncodeSlice(wantPoly, 0)) + wkt.MustEncode(tt.polygon), tmID, geomhelp.WktMustEncode(got[tmID].Geometry, 0), geomhelp.WktMustEncodeSlice(wantPoly, 0)) } } }) From 29898a2acce0580ba5e4118cdadaa35585d5c78e Mon Sep 17 00:00:00 2001 From: Dirk van Bree Date: Tue, 14 Jul 2026 10:45:17 +0200 Subject: [PATCH 22/27] refactor: moved helper function to geomhelp --- geomhelp/geomhelp.go | 15 +++++++++++++++ processing/processing.go | 14 -------------- snap/snap.go | 2 +- snap/snap_test.go | 3 +-- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/geomhelp/geomhelp.go b/geomhelp/geomhelp.go index 758a059..fdef413 100644 --- a/geomhelp/geomhelp.go +++ b/geomhelp/geomhelp.go @@ -162,3 +162,18 @@ func wktMustEncodeTruncated(geom geom.Geometry, width uint) string { } return truncate.StringWithTail(wkt.MustEncode(geom), width, "...") } + +func PolygonSliceToGeom(polygons []geom.Polygon) geom.Geometry { + if len(polygons) == 0 { + panic("Multipolygon with zero polygons encountered") + } + if len(polygons) == 1 { + return polygons[0] + } + multipolygon := make(geom.MultiPolygon, len(polygons)) + for i, p := range polygons { + multipolygon[i] = p + } + return multipolygon +} + diff --git a/processing/processing.go b/processing/processing.go index 9548f0e..1cd29ce 100644 --- a/processing/processing.go +++ b/processing/processing.go @@ -19,20 +19,6 @@ func readFeaturesFromSource(source Source, features chan<- Feature) { source.ReadFeatures(features) } -func PolygonSliceToGeom(polygons []geom.Polygon) geom.Geometry { - if len(polygons) == 0 { - panic("Multipolygon with zero polygons encountered") - } - if len(polygons) == 1 { - return polygons[0] - } - multipolygon := make(geom.MultiPolygon, len(polygons)) - for i, p := range polygons { - multipolygon[i] = p - } - return multipolygon -} - func PolygonsToMulti(polygons []geom.Polygon) geom.MultiPolygon { l := len(polygons) multiPolygon := make(geom.MultiPolygon, l) diff --git a/snap/snap.go b/snap/snap.go index a849fd6..02f8138 100644 --- a/snap/snap.go +++ b/snap/snap.go @@ -68,7 +68,7 @@ func SnapPolygon(polygon geom.Polygon, tileMatrixSet tms20.TileMatrixSet, tmIDs newPolygonsPerTileMatrixID := make(map[tms20.TMID]processing.SnapResult, len(newPolygonsPerLevel)) for level, newPolygons := range newPolygonsPerLevel { tilesbbox := ix.GetPrimitiveQBBox(pointindex.Level(tmIDsByLevels[level])) - newGeometry := processing.PolygonSliceToGeom(newPolygons) + newGeometry := geomhelp.PolygonSliceToGeom(newPolygons) newPolygonsPerTileMatrixID[tmIDsByLevels[level]] = processing.SnapResult{Geometry: newGeometry, Tiles: tilesbbox} } diff --git a/snap/snap_test.go b/snap/snap_test.go index 196c6a3..a11e75b 100644 --- a/snap/snap_test.go +++ b/snap/snap_test.go @@ -7,7 +7,6 @@ import ( "github.com/pdok/texel/geomhelp" "github.com/pdok/texel/mathhelp" "github.com/pdok/texel/pointindex" - "github.com/pdok/texel/processing" "github.com/stretchr/testify/require" "github.com/go-spatial/geom/encoding/wkt" @@ -791,7 +790,7 @@ func TestSnap_snapPolygon(t *testing.T) { } got := SnapPolygon(tt.polygon, tt.tms, tt.tmIDs, tt.config) for tmID, wantPoly := range tt.want { - wantGeom := processing.PolygonSliceToGeom(wantPoly) + wantGeom := geomhelp.PolygonSliceToGeom(wantPoly) if !assert.Equal(t, wantGeom, got[tmID].Geometry) { t.Errorf("snapPolygon(%v, _, %v)\n= %v\nwant: %v", wkt.MustEncode(tt.polygon), tmID, geomhelp.WktMustEncode(got[tmID].Geometry, 0), geomhelp.WktMustEncodeSlice(wantPoly, 0)) From 1dfe3437c563e4596137bb78baaf74193e4eca1c Mon Sep 17 00:00:00 2001 From: Dirk van Bree Date: Tue, 14 Jul 2026 11:39:22 +0200 Subject: [PATCH 23/27] feat: flag for optional encoding of geometries --- main.go | 30 +++++++++++++++++++++--------- processing/gpkg/gpkg.go | 23 +++++++++++++++-------- snap/snap.go | 8 +++++++- 3 files changed, 43 insertions(+), 18 deletions(-) diff --git a/main.go b/main.go index dbd5495..a7dd19e 100644 --- a/main.go +++ b/main.go @@ -25,15 +25,18 @@ import ( "github.com/urfave/cli/v2" ) -const SOURCE string = `sourceGpkg` -const TARGET string = `targetGpkg` -const OVERWRITE string = `overwrite` -const TILEMATRIXSET string = `tilematrixset` -const TILEMATRICES string = `tilematrices` -const PAGESIZE string = `pagesize` -const KEEPPOINTSANDLINES string = `keeppointsandlines` -const IGNOREOUTSIDEGRID string = `ignoreoutsidegrid` -const REVERSEWINDINGORDER string = `reversewindingorder` +const ( + SOURCE string = `sourceGpkg` + TARGET string = `targetGpkg` + OVERWRITE string = `overwrite` + TILEMATRIXSET string = `tilematrixset` + TILEMATRICES string = `tilematrices` + PAGESIZE string = `pagesize` + KEEPPOINTSANDLINES string = `keeppointsandlines` + IGNOREOUTSIDEGRID string = `ignoreoutsidegrid` + REVERSEWINDINGORDER string = `reversewindingorder` + ENCODETILES string = `encodetiles` +) //nolint:funlen func main() { @@ -111,6 +114,14 @@ func main() { Required: false, EnvVars: []string{strcase.ToScreamingSnake(REVERSEWINDINGORDER)}, }, + &cli.BoolFlag{ + Name: ENCODETILES, + Aliases: []string{"enc"}, + Usage: "Add tables with mapbox-encoded geometries.", + Value: false, + Required: false, + EnvVars: []string{strcase.ToScreamingSnake(ENCODETILES)}, + }, } app.Action = func(c *cli.Context) error { @@ -145,6 +156,7 @@ func main() { KeepPointsAndLines: c.Bool(KEEPPOINTSANDLINES), IgnoreOutsideGrid: c.Bool(IGNOREOUTSIDEGRID), ReverseWindingOrder: c.Bool(REVERSEWINDINGORDER), + EncodeTiles: c.Bool(ENCODETILES), } for _, tmID := range tileMatrixIDs { gpkgTargets[tmID] = initGPKGTarget(targetPathFmt, tmID, overwrite, pagesize) diff --git a/processing/gpkg/gpkg.go b/processing/gpkg/gpkg.go index ed335e0..fd1cfa1 100644 --- a/processing/gpkg/gpkg.go +++ b/processing/gpkg/gpkg.go @@ -173,9 +173,10 @@ func (source SourceGeopackage) GetTableInfo() []Table { } type TargetGeopackage struct { - Table Table - pagesize int - handle *gpkg.Handle + Table Table + pagesize int + handle *gpkg.Handle + encodeTiles bool } func (target *TargetGeopackage) Init(file string, pagesize int) { @@ -199,9 +200,11 @@ func (target *TargetGeopackage) CreateTables(tables []Table) error { return err } - err = buildEncodedTable(target.handle, table) - if err != nil { - return err + if target.encodeTiles { + err = buildEncodedTable(target.handle, table) + if err != nil { + return err + } } } return nil @@ -214,14 +217,18 @@ func (target *TargetGeopackage) WriteFeatures(inFeatures <-chan processing.Featu feature, hasMore := <-inFeatures if !hasMore { target.writeFeatures(features) - target.writeEncodedFeatures(features) + if target.encodeTiles { + target.writeEncodedFeatures(features) + } break } features = append(features, feature) if len(features)%target.pagesize == 0 { target.writeFeatures(features) - target.writeEncodedFeatures(features) + if target.encodeTiles { + target.writeEncodedFeatures(features) + } features = nil } } diff --git a/snap/snap.go b/snap/snap.go index 02f8138..bb007cb 100644 --- a/snap/snap.go +++ b/snap/snap.go @@ -34,6 +34,7 @@ type Config struct { KeepPointsAndLines bool IgnoreOutsideGrid bool ReverseWindingOrder bool + EncodeTiles bool } // SnapPolygon snaps polygons' points to a tile's internal pixel grid @@ -67,7 +68,12 @@ func SnapPolygon(polygon geom.Polygon, tileMatrixSet tms20.TileMatrixSet, tmIDs newPolygonsPerTileMatrixID := make(map[tms20.TMID]processing.SnapResult, len(newPolygonsPerLevel)) for level, newPolygons := range newPolygonsPerLevel { - tilesbbox := ix.GetPrimitiveQBBox(pointindex.Level(tmIDsByLevels[level])) + var tilesbbox []pointindex.Quadrant + if config.EncodeTiles { + tilesbbox = ix.GetPrimitiveQBBox(pointindex.Level(tmIDsByLevels[level])) + } else { + tilesbbox = []pointindex.Quadrant{} + } newGeometry := geomhelp.PolygonSliceToGeom(newPolygons) newPolygonsPerTileMatrixID[tmIDsByLevels[level]] = processing.SnapResult{Geometry: newGeometry, Tiles: tilesbbox} } From bb69eac2e7316cb41b5567b4dbc0ccb94be3cdec Mon Sep 17 00:00:00 2001 From: Dirk van Bree Date: Tue, 14 Jul 2026 15:13:42 +0200 Subject: [PATCH 24/27] chore: linter warnings (mainly in copied code) --- geomhelp/geomhelp.go | 1 - pointindex/pointindex.go | 2 +- snap/snap.go | 2 +- tile/go_spatial_feature.go | 339 +++++++------------------------------ tile/tile.go | 15 +- 5 files changed, 69 insertions(+), 290 deletions(-) diff --git a/geomhelp/geomhelp.go b/geomhelp/geomhelp.go index fdef413..8e1b233 100644 --- a/geomhelp/geomhelp.go +++ b/geomhelp/geomhelp.go @@ -176,4 +176,3 @@ func PolygonSliceToGeom(polygons []geom.Polygon) geom.Geometry { } return multipolygon } - diff --git a/pointindex/pointindex.go b/pointindex/pointindex.go index d06e903..97e67d1 100644 --- a/pointindex/pointindex.go +++ b/pointindex/pointindex.go @@ -144,7 +144,7 @@ func (ix *PointIndex) GetPrimitiveQBBox(l Level) []Quadrant { intExtent: extent, intCentroid: centroid, } - quadrantSlice[i * (maxY - minY) + j] = newQuadrant + quadrantSlice[i*(maxY-minY)+j] = newQuadrant } } return quadrantSlice diff --git a/snap/snap.go b/snap/snap.go index bb007cb..226d130 100644 --- a/snap/snap.go +++ b/snap/snap.go @@ -70,7 +70,7 @@ func SnapPolygon(polygon geom.Polygon, tileMatrixSet tms20.TileMatrixSet, tmIDs for level, newPolygons := range newPolygonsPerLevel { var tilesbbox []pointindex.Quadrant if config.EncodeTiles { - tilesbbox = ix.GetPrimitiveQBBox(pointindex.Level(tmIDsByLevels[level])) + tilesbbox = ix.GetPrimitiveQBBox(pointindex.Level(tmIDsByLevels[level])) //nolint:gosec // G115 These are numbers < 40 } else { tilesbbox = []pointindex.Quadrant{} } diff --git a/tile/go_spatial_feature.go b/tile/go_spatial_feature.go index 155395e..7d348a6 100644 --- a/tile/go_spatial_feature.go +++ b/tile/go_spatial_feature.go @@ -9,6 +9,7 @@ package tile // - Removed unused parameter "context" from all functions. import ( + "errors" "fmt" "log" @@ -19,34 +20,36 @@ import ( // START copied from vector_tile.pb.go -type Tile_GeomType int32 +type GSTileGeomType int32 const ( - Tile_UNKNOWN Tile_GeomType = 0 - Tile_POINT Tile_GeomType = 1 - Tile_LINESTRING Tile_GeomType = 2 - Tile_POLYGON Tile_GeomType = 3 + TileUNKNOWN GSTileGeomType = 0 + TilePOINT GSTileGeomType = 1 + TileLINESTRING GSTileGeomType = 2 + TilePOLYGON GSTileGeomType = 3 ) -var Tile_GeomType_name = map[int32]string{ +var TileGeomTypeName = map[int32]string{ 0: "UNKNOWN", 1: "POINT", 2: "LINESTRING", 3: "POLYGON", } -var Tile_GeomType_value = map[string]int32{ + +var TileGeomTypeValue = map[string]int32{ "UNKNOWN": 0, "POINT": 1, "LINESTRING": 2, "POLYGON": 3, } + var ( - ErrNilFeature = fmt.Errorf("feature is nil") - ErrUnknownGeometryType = fmt.Errorf("unknown geometry type") - ErrNilGeometryType = fmt.Errorf("geometry is nil") + ErrNilFeature = errors.New("feature is nil") + ErrUnknownGeometryType = errors.New("unknown geometry type") + ErrNilGeometryType = errors.New("geometry is nil") ) -type Tile_Feature struct { +type GSTileFeature struct { Id *uint64 `protobuf:"varint,1,opt,name=id,def=0" json:"id,omitempty"` // Tags of this feature are encoded as repeated pairs of // integers. @@ -54,12 +57,12 @@ type Tile_Feature struct { // 4.2 and 4.4 of the specification Tags []uint32 `protobuf:"varint,2,rep,packed,name=tags" json:"tags,omitempty"` // The type of geometry stored in this feature. - Type *Tile_GeomType `protobuf:"varint,3,opt,name=type,enum=vector_tile.Tile_GeomType,def=0" json:"type,omitempty"` + Type *GSTileGeomType `protobuf:"varint,3,opt,name=type,enum=vector_tile.Tile_GeomType,def=0" json:"type,omitempty"` // Contains a stream of commands and parameters (vertices). // A detailed description on geometry encoding is located in // section 4.3 of the specification. - Geometry []uint32 `protobuf:"varint,4,rep,packed,name=geometry" json:"geometry,omitempty"` - XXX_unrecognized []byte `json:"-"` + Geometry []uint32 `protobuf:"varint,4,rep,packed,name=geometry" json:"geometry,omitempty"` + XXXunrecognized []byte `json:"-"` } // END copied from vector_tile.pb.go @@ -69,7 +72,6 @@ const debug = false // Rest of file is copied from feature.go - // TODO: Need to put in validation for the Geometry, as current the system // does not check to make sure that the geometry is following the rules as // laid out by the spec (i.e. polygons must not have the same start and end @@ -78,9 +80,9 @@ const debug = false // Feature describes a feature of a Layer. A layer will contain multiple features // each of which has a geometry describing the interesting thing, and the metadata // associated with it. -type Feature struct { +type Feature struct { //nolint:recvcheck ID *uint64 - Tags map[string]interface{} + Tags map[string]any Geometry geom.Geometry } @@ -98,7 +100,7 @@ func (f Feature) String() string { } // NewFeatures returns one or more features for the given Geometry -func NewFeatures(geo geom.Geometry, tags map[string]interface{}) (f []Feature) { +func NewFeatures(geo geom.Geometry, tags map[string]any) (f []Feature) { if geo == nil { return f // return empty feature set for a nil geometry } @@ -120,8 +122,8 @@ func NewFeatures(geo geom.Geometry, tags map[string]interface{}) (f []Feature) { } // VTileFeature will return a vectorTile.Feature that would represent the Feature -func (f *Feature) VTileFeature(keys []string, vals []interface{}) (tf *Tile_Feature, err error) { - tf = new(Tile_Feature) +func (f *Feature) VTileFeature(keys []string, vals []any) (tf *GSTileFeature, err error) { + tf = new(GSTileFeature) tf.Id = f.ID if tf.Tags, err = keyvalTagsMap(keys, vals, f); err != nil { @@ -149,22 +151,22 @@ const ( cmdLineTo uint32 = 2 cmdClosePath uint32 = 7 - maxCmdCount uint32 = 0x1FFFFFFF + // maxCmdCount uint32 = 0x1FFFFFFF ) type Command uint32 // NewCommand return a new command encoder func NewCommand(cmd uint32, count int) Command { - return Command((cmd & 0x7) | (uint32(count) << 3)) + return Command((cmd & 0x7) | (uint32(count) << 3)) //nolint:gosec // G115 - This is potentially an issue if we ever get huge polygons } -//ID encodes the ID of the command +// ID encodes the ID of the command func (c Command) ID() uint32 { return uint32(c) & 0x7 } -//Count encode the count of elements in the command +// Count encode the count of elements in the command func (c Command) Count() int { return int(uint32(c) >> 3) } @@ -184,7 +186,7 @@ func (c Command) String() string { // encodeZigZag does the ZigZag encoding for small ints. func encodeZigZag(i int64) uint32 { - return uint32((i << 1) ^ (i >> 31)) + return uint32((i << 1) ^ (i >> 31)) //nolint:gosec // G115 This can go wrong with polygons with enormous edges } // cursor reprsents the current position, this is needed to encode the geometry. @@ -197,7 +199,7 @@ type cursor struct { } // NewCursor creates a new cursor for drawing and MVT tile -func NewCursor() *cursor { +func NewCursor() *cursor { //nolint:revive return &cursor{} } @@ -208,6 +210,21 @@ func (c *cursor) GetDeltaPointAndUpdate(p geom.Point) (dx, dy int64) { return delta[0][0], delta[0][1] } +// MoveTo encodes a move to command for the given points +func (c *cursor) MoveTo(points ...[2]float64) []uint32 { + return c.encodeCmd(uint32(NewCommand(cmdMoveTo, len(points))), points) +} + +// LineTo encodes a line to command for the given points +func (c *cursor) LineTo(points ...[2]float64) []uint32 { + return c.encodeCmd(uint32(NewCommand(cmdLineTo, len(points))), points) +} + +// ClosePath encodes a close path command +func (c *cursor) ClosePath() uint32 { + return uint32(NewCommand(cmdClosePath, 1)) +} + func (c *cursor) moveCursorPoints(pts ...[2]int64) (deltas [][2]int64) { deltas = make([][2]int64, len(pts)) for i := range pts { @@ -246,7 +263,6 @@ func (c *cursor) encodeCmd(cmd uint32, points [][2]float64) []uint32 { } func (c *cursor) encodeLinearRing(order winding.Order, wo winding.Winding, ring [][2]float64) []uint32 { - iring := make([][2]int64, len(ring)) for i := range iring { // the process of truncating the float can cause the winding order to flip! @@ -331,26 +347,11 @@ func (c *cursor) encodePolygon(geo geom.Polygon) []uint32 { return g } -// MoveTo encodes a move to command for the given points -func (c *cursor) MoveTo(points ...[2]float64) []uint32 { - return c.encodeCmd(uint32(NewCommand(cmdMoveTo, len(points))), points) -} - -// LineTo encodes a line to command for the given points -func (c *cursor) LineTo(points ...[2]float64) []uint32 { - return c.encodeCmd(uint32(NewCommand(cmdLineTo, len(points))), points) -} - -// ClosePath encodes a close path command -func (c *cursor) ClosePath() uint32 { - return uint32(NewCommand(cmdClosePath, 1)) -} - // EncodeGeometry will take a geom.Geometry and encode it according to the // mapbox vector_tile spec. -func EncodeGeometry(geometry geom.Geometry) (g []uint32, vtyp Tile_GeomType, err error) { +func EncodeGeometry(geometry geom.Geometry) (g []uint32, vtyp GSTileGeomType, err error) { if geometry == nil { - return nil, Tile_UNKNOWN, ErrNilGeometryType + return nil, TileUNKNOWN, ErrNilGeometryType } c := NewCursor() @@ -358,17 +359,17 @@ func EncodeGeometry(geometry geom.Geometry) (g []uint32, vtyp Tile_GeomType, err switch t := geometry.(type) { case geom.Point: g = append(g, c.MoveTo(t)...) - return g, Tile_POINT, nil + return g, TilePOINT, nil case geom.MultiPoint: g = append(g, c.MoveTo(t.Points()...)...) - return g, Tile_POINT, nil + return g, TilePOINT, nil case geom.LineString: points := t.Vertices() g = append(g, c.MoveTo(points[0])...) g = append(g, c.LineTo(points[1:]...)...) - return g, Tile_LINESTRING, nil + return g, TileLINESTRING, nil case geom.MultiLineString: lines := t.LineStrings() @@ -377,259 +378,39 @@ func EncodeGeometry(geometry geom.Geometry) (g []uint32, vtyp Tile_GeomType, err g = append(g, c.MoveTo(points[0])...) g = append(g, c.LineTo(points[1:]...)...) } - return g, Tile_LINESTRING, nil + return g, TileLINESTRING, nil case geom.Polygon: g = append(g, c.encodePolygon(t)...) - return g, Tile_POLYGON, nil + return g, TilePOLYGON, nil case geom.MultiPolygon: polygons := t.Polygons() for _, p := range polygons { g = append(g, c.encodePolygon(p)...) } - return g, Tile_POLYGON, nil + return g, TilePOLYGON, nil case *geom.MultiPolygon: if t == nil { - return g, Tile_POLYGON, nil + return g, TilePOLYGON, nil } polygons := t.Polygons() for _, p := range polygons { g = append(g, c.encodePolygon(p)...) } - return g, Tile_POLYGON, nil + return g, TilePOLYGON, nil default: - return nil, Tile_UNKNOWN, ErrUnknownGeometryType + return nil, TileUNKNOWN, ErrUnknownGeometryType } } -// keyvalMapsFromFeatures returns a key map and value map, to help with the translation -// to mapbox tile format. In the Tile format, the Tile contains a mapping of all the unique -// keys and values, and then each feature contains a vector map to these two. This is an -// intermediate data structure to help with the construction of the three mappings. -func keyvalMapsFromFeatures(features []Feature) (keyMap []string, valMap []interface{}, err error) { - var didFind bool - for _, f := range features { - for k, v := range f.Tags { - didFind = false - for _, mk := range keyMap { - if k == mk { - didFind = true - break - } - } - if !didFind { - keyMap = append(keyMap, k) - } - didFind = false - - switch vt := v.(type) { - default: - if vt == nil { - // ignore nil types - continue - } - return keyMap, valMap, fmt.Errorf("unsupported type for value(%v) with key(%v) in tags for feature %v.", vt, k, f) - - case string: - for _, mv := range valMap { - tmv, ok := mv.(string) - if !ok { - continue - } - if tmv == vt { - didFind = true - break - } - } - - case fmt.Stringer: - for _, mv := range valMap { - tmv, ok := mv.(fmt.Stringer) - if !ok { - continue - } - if tmv.String() == vt.String() { - didFind = true - break - } - } - - case int: - for _, mv := range valMap { - tmv, ok := mv.(int) - if !ok { - continue - } - if tmv == vt { - didFind = true - break - } - } - - case int8: - for _, mv := range valMap { - tmv, ok := mv.(int8) - if !ok { - continue - } - if tmv == vt { - didFind = true - break - } - } - - case int16: - for _, mv := range valMap { - tmv, ok := mv.(int16) - if !ok { - continue - } - if tmv == vt { - didFind = true - break - } - } - - case int32: - for _, mv := range valMap { - tmv, ok := mv.(int32) - if !ok { - continue - } - if tmv == vt { - didFind = true - break - } - } - - case int64: - for _, mv := range valMap { - tmv, ok := mv.(int64) - if !ok { - continue - } - if tmv == vt { - didFind = true - break - } - } - - case uint: - for _, mv := range valMap { - tmv, ok := mv.(uint) - if !ok { - continue - } - if tmv == vt { - didFind = true - break - } - } - - case uint8: - for _, mv := range valMap { - tmv, ok := mv.(uint8) - if !ok { - continue - } - if tmv == vt { - didFind = true - break - } - } - - case uint16: - for _, mv := range valMap { - tmv, ok := mv.(uint16) - if !ok { - continue - } - if tmv == vt { - didFind = true - break - } - } - - case uint32: - for _, mv := range valMap { - tmv, ok := mv.(uint32) - if !ok { - continue - } - if tmv == vt { - didFind = true - break - } - } - - case uint64: - for _, mv := range valMap { - tmv, ok := mv.(uint64) - if !ok { - continue - } - if tmv == vt { - didFind = true - break - } - } - - case float32: - for _, mv := range valMap { - tmv, ok := mv.(float32) - if !ok { - continue - } - if tmv == vt { - didFind = true - break - } - } - - case float64: - for _, mv := range valMap { - tmv, ok := mv.(float64) - if !ok { - continue - } - if tmv == vt { - didFind = true - break - } - } - - case bool: - for _, mv := range valMap { - tmv, ok := mv.(bool) - if !ok { - continue - } - if tmv == vt { - didFind = true - break - } - } - - } // value type switch - - if !didFind { - valMap = append(valMap, v) - } - - } // For f.Tags - } // for features - return keyMap, valMap, nil -} - // keyvalTagsMap will return the tags map as expected by the mapbox tile spec. It takes -// a keyMap and a valueMap that list the the order of the expected keys and values. It will +// a keyMap and a valueMap that list the order of the expected keys and values. It will // return a vector map that refers to these two maps. -func keyvalTagsMap(keyMap []string, valueMap []interface{}, f *Feature) (tags []uint32, err error) { - +func keyvalTagsMap(keyMap []string, valueMap []any, f *Feature) (tags []uint32, err error) { //nolint: cyclop,funlen if f == nil { return nil, ErrNilFeature } @@ -650,7 +431,7 @@ func keyvalTagsMap(keyMap []string, valueMap []interface{}, f *Feature) (tags [] if kidx == -1 { log.Printf("did not find key (%v) in keymap.", key) - return tags, fmt.Errorf("did not find key (%v) in keymap.", key) + return tags, fmt.Errorf("did not find key (%v) in keymap", key) } // if val is nil we skip it for now @@ -662,7 +443,7 @@ func keyvalTagsMap(keyMap []string, valueMap []interface{}, f *Feature) (tags [] for i, v := range valueMap { switch tv := val.(type) { default: - return tags, fmt.Errorf("value (%[1]v) of type (%[1]T) for key (%[2]v) is not supported.", tv, key) + return tags, fmt.Errorf("value (%[1]v) of type (%[1]T) for key (%[2]v) is not supported", tv, key) case string: vmt, ok := v.(string) // Make sure the type of the Value map matches the type of the Tag's value if !ok || vmt != tv { // and that the values match @@ -746,9 +527,9 @@ func keyvalTagsMap(keyMap []string, valueMap []interface{}, f *Feature) (tags [] } // range on value if vidx == -1 { // None of the values matched. - return tags, fmt.Errorf("did not find a value: %v in valuemap.", val) + return tags, fmt.Errorf("did not find a value: %v in valuemap", val) } - tags = append(tags, uint32(kidx), uint32(vidx)) + tags = append(tags, uint32(kidx), uint32(vidx)) //nolint: gosec // G115 casting sems unavoidable } // Move to the next tag key and value. return tags, nil diff --git a/tile/tile.go b/tile/tile.go index 0ef671d..2310eb8 100644 --- a/tile/tile.go +++ b/tile/tile.go @@ -19,23 +19,22 @@ func MvtEncodeGeometry(q pointindex.Quadrant, g geom.Geometry) EncodedGeometry { ext := q.Extent() preparedGeo := mvt.PrepareGeo(g, &ext, float64(precision)) - // This should not be necessary. -// sg, err := convert.ToTegola(preparedGeo) -// tegolaGeo, err := validate.CleanGeometry(context.TODO(), sg, &ext) -// validatedGeo := convert.ToGeom(tegolaGeo) + // This should not be necessary. + // sg, err := convert.ToTegola(preparedGeo) + // tegolaGeo, err := validate.CleanGeometry(context.TODO(), sg, &ext) + // validatedGeo := convert.ToGeom(tegolaGeo) encgeom, geomtype, err := EncodeGeometry(preparedGeo) if err != nil { panic(err) } - xTile, yTile := q.Coords() return EncodedGeometry{ - Encoding: encgeom, + Encoding: encgeom, GeometryType: int32(geomtype), - XTile: xTile, - YTile: yTile, + XTile: xTile, + YTile: yTile, } } From 5e4c7582f5716a3418649c6260f1daeff8920bd0 Mon Sep 17 00:00:00 2001 From: Dirk van Bree Date: Tue, 14 Jul 2026 15:15:54 +0200 Subject: [PATCH 25/27] refactor: removed unused code --- intgeom/polygon.go | 29 ----------------------------- 1 file changed, 29 deletions(-) delete mode 100644 intgeom/polygon.go diff --git a/intgeom/polygon.go b/intgeom/polygon.go deleted file mode 100644 index f6f47b7..0000000 --- a/intgeom/polygon.go +++ /dev/null @@ -1,29 +0,0 @@ -package intgeom - -import ( - "github.com/go-spatial/geom" -) - -type Polygon [][][2]M - -func (p Polygon) ToGeomPolygon() geom.Polygon { - geompol := make([][][2]float64, len(p)) - for i, ring := range p { - geompol[i] = make([][2]float64, len(ring)) - for j, point := range ring { - geompol[i][j] = [2]float64{ToGeomOrd(point[0]), ToGeomOrd(point[1])} - } - } - return geompol -} - -func FromGeomPolygon(p geom.Polygon) Polygon { - intpol := make([][][2]int64, len(p)) - for i, ring := range p { - intpol[i] = make([][2]int64, len(ring)) - for j, point := range ring { - intpol[i][j] = [2]int64{FromGeomOrd(point[0]), FromGeomOrd(point[1])} - } - } - return intpol -} From 7bb1c18ee9a0c90a38743d7d4ae8c55796b110b3 Mon Sep 17 00:00:00 2001 From: Dirk van Bree Date: Fri, 17 Jul 2026 11:43:07 +0200 Subject: [PATCH 26/27] refactor: remove unused function --- processing/processing.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/processing/processing.go b/processing/processing.go index 1cd29ce..7a92a3c 100644 --- a/processing/processing.go +++ b/processing/processing.go @@ -19,15 +19,6 @@ func readFeaturesFromSource(source Source, features chan<- Feature) { source.ReadFeatures(features) } -func PolygonsToMulti(polygons []geom.Polygon) geom.MultiPolygon { - l := len(polygons) - multiPolygon := make(geom.MultiPolygon, l) - for i := range l { - multiPolygon[i] = polygons[i] - } - return multiPolygon -} - func mergeGeometries(g, h geom.Geometry) geom.Geometry { if g == nil { return h From 4e07861c1eee177ac561fff5531450d36197fdae Mon Sep 17 00:00:00 2001 From: Dirk van Bree Date: Fri, 17 Jul 2026 12:04:22 +0200 Subject: [PATCH 27/27] test: add PolygonSliceToGeom unit test --- geomhelp/geomhelp_test.go | 49 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 geomhelp/geomhelp_test.go diff --git a/geomhelp/geomhelp_test.go b/geomhelp/geomhelp_test.go new file mode 100644 index 0000000..7cd2c56 --- /dev/null +++ b/geomhelp/geomhelp_test.go @@ -0,0 +1,49 @@ +package geomhelp + +import ( + "testing" + + "github.com/go-spatial/geom" + "github.com/stretchr/testify/assert" +) + +func TestGeomhelp_PolygonSliceToGeom(t *testing.T) { + tests := []struct { + name string + polygonSlice []geom.Polygon + want geom.Geometry + wantPanic bool + }{ + { + name: "Empty polygon slice", + polygonSlice: []geom.Polygon{}, + wantPanic: true, + }, + { + name: "Single polygon", + polygonSlice: []geom.Polygon{{{{0.0, 0.0}, {1.0, 0.0}, {1.0, 1.0}}}}, + want: geom.Polygon{{{0.0, 0.0}, {1.0, 0.0}, {1.0, 1.0}}}, + }, + { + name: "Two polygons", + polygonSlice: []geom.Polygon{ + {{{0.0, 0.0}, {1.0, 0.0}, {1.0, 1.0}}}, + {{{10.0, 10.0}, {10.0, 11.0}, {11.0, 11.0}}}, + }, + want: geom.MultiPolygon{ + {{{0.0, 0.0}, {1.0, 0.0}, {1.0, 1.0}}}, + {{{10.0, 10.0}, {10.0, 11.0}, {11.0, 11.0}}}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.wantPanic { + assert.Panics(t, func() { PolygonSliceToGeom(tt.polygonSlice) }) + return + } + got := PolygonSliceToGeom(tt.polygonSlice) + assert.Equal(t, tt.want, got) + }) + } +}