Add geometry info to h3 aggregation table

This makes it possible to use Tegola to show vector tiles.
This commit is contained in:
Eli Ribble 2025-11-15 21:33:01 +00:00
parent 6f6af23578
commit a14249710d
No known key found for this signature in database
6 changed files with 101 additions and 30 deletions

View file

@ -177,11 +177,16 @@ func updateSummaryTables(ctx context.Context, org *models.Organization) {
cellToCount[cell] = cellToCount[cell] + 1 cellToCount[cell] = cellToCount[cell] + 1
} }
var to_insert []bob.Mod[*dialect.InsertQuery] = make([]bob.Mod[*dialect.InsertQuery], 0) var to_insert []bob.Mod[*dialect.InsertQuery] = make([]bob.Mod[*dialect.InsertQuery], 0)
to_insert = append(to_insert, im.Into("h3_aggregation", "cell", "resolution", "count_", "type_", "organization_id")) to_insert = append(to_insert, im.Into("h3_aggregation", "cell", "resolution", "count_", "type_", "organization_id", "geometry"))
for cell, count := range cellToCount { for cell, count := range cellToCount {
to_insert = append(to_insert, im.Values(psql.Arg(cell.String(), i, count, enums.H3aggregationtypeServicerequest, org.ID))) polygon, err := cellToPostgisGeometry(cell)
if err != nil {
log.Error().Err(err).Msg("Failed to get PostGIS geometry")
continue
}
// log.Info().Str("polygon", polygon).Msg("Going to insert")
to_insert = append(to_insert, im.Values(psql.Arg(cell.String(), i, count, enums.H3aggregationtypeServicerequest, org.ID), psql.F("st_geomfromtext", psql.S(polygon), 4326)))
} }
//to_insert = append(to_insert, im.OnConflict("h3_aggregation_cell_organization_id_type__key").DoUpdate(
to_insert = append(to_insert, im.OnConflict("cell, organization_id, type_").DoUpdate( to_insert = append(to_insert, im.OnConflict("cell, organization_id, type_").DoUpdate(
im.SetCol("count_").To(psql.Raw("EXCLUDED.count_")), im.SetCol("count_").To(psql.Raw("EXCLUDED.count_")),
)) ))

30
h3.go
View file

@ -2,6 +2,7 @@ package main
import ( import (
"fmt" "fmt"
"strings"
"github.com/Gleipnir-Technology/go-geojson2h3" "github.com/Gleipnir-Technology/go-geojson2h3"
"github.com/tidwall/geojson" "github.com/tidwall/geojson"
@ -35,6 +36,15 @@ func h3ToGeoJSON(indexes []h3.Cell) (string, error) {
} }
return featureCollection.JSON(), nil return featureCollection.JSON(), nil
} }
func sampleGeoJSON() (string, error) {
indexes := h3Indexes()
featureCollection, err := geojson2h3.ToFeatureCollection(indexes)
if err != nil {
return "", fmt.Errorf("Failed to get feature collection: %w", err)
}
return featureCollection.JSON(), nil
}
func main2() { func main2() {
resolution := 9 resolution := 9
object, err := geojson.Parse(`{"type":"FeatureCollection","features":[{"type":"Feature","properties":{"shape":"Polygon","name":"Unnamed Layer","category":"default"},"geometry":{"type":"Polygon","coordinates":[[[-73.901303,40.756892],[-73.893924,40.743755],[-73.871476,40.756278],[-73.863378,40.764175],[-73.871444,40.768467],[-73.879852,40.760014],[-73.885515,40.764045],[-73.891522,40.761054],[-73.901303,40.756892]]]},"id":"a6ca1b7e-9ddf-4425-ad07-8a895f7d6ccf"}]}`, nil) object, err := geojson.Parse(`{"type":"FeatureCollection","features":[{"type":"Feature","properties":{"shape":"Polygon","name":"Unnamed Layer","category":"default"},"geometry":{"type":"Polygon","coordinates":[[[-73.901303,40.756892],[-73.893924,40.743755],[-73.871476,40.756278],[-73.863378,40.764175],[-73.871444,40.768467],[-73.879852,40.760014],[-73.885515,40.764045],[-73.891522,40.761054],[-73.901303,40.756892]]]},"id":"a6ca1b7e-9ddf-4425-ad07-8a895f7d6ccf"}]}`, nil)
@ -79,3 +89,23 @@ func getCell(x, y float64, resolution int) (h3.Cell, error) {
latLng := h3.NewLatLng(y, x) latLng := h3.NewLatLng(y, x)
return h3.LatLngToCell(latLng, resolution) return h3.LatLngToCell(latLng, resolution)
} }
func cellToPostgisGeometry(c h3.Cell) (string, error) {
boundary, err := h3.CellToBoundary(c)
if err != nil {
return "", fmt.Errorf("Failed to get cell boundary: %w", err)
}
var sb strings.Builder
for i, p := range boundary {
if i > 0 {
sb.WriteString(",")
}
fmt.Fprintf(&sb, "%g %g", p.Lng, p.Lat)
}
// add the first point on to the end to close the polygon
sb.WriteString(",")
fmt.Fprintf(&sb, "%g %g", boundary[0].Lng, boundary[0].Lat)
return fmt.Sprintf("POLYGON((%s))", sb.String()), nil
}

View file

@ -166,7 +166,7 @@ func extractInitials(name string) string {
} }
func htmlDashboard(ctx context.Context, w http.ResponseWriter, user *models.User) { func htmlDashboard(ctx context.Context, w http.ResponseWriter, user *models.User) {
geo, err := h3ToGeoJSON(h3Indexes()) geo, err := sampleGeoJSON()
if err != nil { if err != nil {
respondError(w, "Failed to get geo", err, http.StatusInternalServerError) respondError(w, "Failed to get geo", err, http.StatusInternalServerError)
return return

View file

@ -111,6 +111,7 @@ func main() {
// Authenticated endpoints // Authenticated endpoints
r.Method("GET", "/settings", NewEnsureAuth(getSettings)) r.Method("GET", "/settings", NewEnsureAuth(getSettings))
r.Method("GET", "/vector-tiles/{org_id}/{tileset_id}/{zoom}/{x}/{y}.{format}", NewEnsureAuth(getVectorTiles))
localFS := http.Dir("./static") localFS := http.Dir("./static")
FileServer(r, "/static", localFS, embeddedStaticFS, "static") FileServer(r, "/static", localFS, embeddedStaticFS, "static")

View file

@ -8,10 +8,11 @@ CREATE TYPE H3AggregationType AS ENUM (
CREATE TABLE h3_aggregation ( CREATE TABLE h3_aggregation (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
cell h3index NOT NULL, cell h3index NOT NULL,
resolution INT NOT NULL,
count_ INTEGER NOT NULL, count_ INTEGER NOT NULL,
type_ H3AggregationType NOT NULL, geometry public.geometry(Polygon,4326),
organization_id INTEGER REFERENCES organization (id) NOT NULL, organization_id INTEGER REFERENCES organization (id) NOT NULL,
resolution INT NOT NULL,
type_ H3AggregationType NOT NULL,
UNIQUE(cell, organization_id, type_)); UNIQUE(cell, organization_id, type_));
-- +goose Down -- +goose Down

View file

@ -11,36 +11,70 @@ function onLoad() {
mapboxgl.accessToken = {{ .MapboxToken }}; mapboxgl.accessToken = {{ .MapboxToken }};
const map = new mapboxgl.Map({ const map = new mapboxgl.Map({
container: 'map', // container ID container: 'map', // container ID
style: 'mapbox://styles/mapbox/streets-v12', // style URL style: 'mapbox://styles/mapbox/standard', // style URL
center: [-74.5, 40], // starting position [lng, lat] center: [-119.3, 36.327], // starting position [lng, lat]
zoom: 9, // starting zoom //center: [7.01, 50.74],
zoom: 9 // starting zoom
}); });
map.on("load", function() { map.on("load", function() {
console.log("Map post-load..."); console.log("Map post-load...");
const sourceId = 'h3-hexes'; map.addSource('tegola-bonn', {
const layerId = 'h3-hexes-layer'; 'type': 'vector',
let source = map.getSource(sourceId); 'tiles': [
//'https://tiles.mapillary.com/maps/vtp/mly1_public/2/{z}/{x}/{y}?access_token=MLY|4142433049200173|72206abe5035850d6743b23a49c41333'
if (!source) { 'https://tegola.nidus.cloud/maps/bonn/{z}/{x}/{y}'
map.addSource(sourceId, { ]
type: 'geojson', //'minzoom': 6,
data: geojson //'maxzoom': 14
});
map.addSource('tegola-nidus', {
'type': 'vector',
'tiles': [
//'https://tiles.mapillary.com/maps/vtp/mly1_public/2/{z}/{x}/{y}?access_token=MLY|4142433049200173|72206abe5035850d6743b23a49c41333'
'https://tegola.nidus.cloud/maps/nidus/{z}/{x}/{y}'
]
//'minzoom': 6,
//'maxzoom': 14
}); });
map.addLayer({ map.addLayer({
id: layerId, 'id': 'bonn', // Layer ID
source: sourceId, 'type': 'fill',
type: 'fill', 'source': 'tegola-bonn', // ID of the tile source created above
interactive: false, 'source-layer': 'lakes',
paint: { 'layout': {
'fill-color': '#F00000', 'line-cap': 'round',
'fill-opacity': 0.3 'line-join': 'round'
},
'paint': {
'fill-opacity': 0.1,
'line-opacity': 0.6,
'line-color': 'rgb(53, 175, 109)',
'line-width': 2
} }
//slot: 'middle' // middle slot in Mapbox Standard style
}); });
source = map.getSource(sourceId); map.addLayer({
'id': 'nidus', // Layer ID
'type': 'fill',
'source': 'tegola-nidus', // ID of the tile source created above
'source-layer': 'h3_aggregation',
'layout': {
'line-cap': 'round',
'line-join': 'round'
},
'paint': {
'fill-opacity': 0.3,
'line-opacity': 0.6,
'line-color': 'rgb(53, 175, 109)',
'line-width': 2
} }
source.setData(geojson); //slot: 'middle' // middle slot in Mapbox Standard style
});
console.log("Map post-load done."); console.log("Map post-load done.");
}); });
map.addControl(new mapboxgl.NavigationControl());
console.log("Map init done."); console.log("Map init done.");
} }
window.addEventListener("load", onLoad); window.addEventListener("load", onLoad);