From 3743d636925ebdb5dd05ae3c10ede11fbb0f4441 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Wed, 11 Mar 2026 14:28:59 +0000 Subject: [PATCH] Add proxy for managing tiles --- api/routes.go | 2 +- api/tile.go | 67 +++++++++- html/static/js/map-proxied-arcgis-tile.js | 153 ++++++++++++++++++++++ html/static/js/map-with-markers.js | 114 ---------------- html/template/sync/review/pool.html | 42 +++++- platform/imagetile/empty-tile.png | Bin 0 -> 3917 bytes platform/imagetile/imagetile.go | 54 +++++++- rmo/status.go | 75 +---------- sync/review.go | 5 + userfile/base.go | 2 + 10 files changed, 316 insertions(+), 198 deletions(-) create mode 100644 html/static/js/map-proxied-arcgis-tile.js delete mode 100644 html/static/js/map-with-markers.js create mode 100644 platform/imagetile/empty-tile.png diff --git a/api/routes.go b/api/routes.go index 3f634394..0a77a0d5 100644 --- a/api/routes.go +++ b/api/routes.go @@ -24,7 +24,7 @@ func AddRoutes(r chi.Router) { r.Method("POST", "/image/{uuid}/content", auth.NewEnsureAuth(apiImageContentPost)) r.Method("GET", "/leads", authenticatedHandlerJSON(listLead)) r.Method("POST", "/leads", authenticatedHandlerJSONPost(postLeads)) - r.Method("GET", "/tile//{z}/{y}/{x}", auth.NewEnsureAuth(getTile)) + r.Method("GET", "/tile/{z}/{y}/{x}", auth.NewEnsureAuth(getTile)) // Unauthenticated endpoints r.Get("/district", apiGetDistrict) diff --git a/api/tile.go b/api/tile.go index 3b96daa6..c7873624 100644 --- a/api/tile.go +++ b/api/tile.go @@ -1,12 +1,21 @@ package api import ( + "bytes" + "context" + "errors" "fmt" + "io" "net/http" + "os" + "path/filepath" "strconv" + "github.com/Gleipnir-Technology/nidus-sync/config" "github.com/Gleipnir-Technology/nidus-sync/db/models" + "github.com/Gleipnir-Technology/nidus-sync/platform/imagetile" "github.com/go-chi/chi/v5" + "github.com/rs/zerolog/log" ) func getTile(w http.ResponseWriter, r *http.Request, org *models.Organization, user *models.User) { @@ -29,5 +38,61 @@ func getTile(w http.ResponseWriter, r *http.Request, org *models.Organization, u http.Error(w, "can't parse x as an integer", http.StatusBadRequest) return } - fmt.Fprintf(w, "%d, %d, %d", x, y, z) + err = handleTile(r.Context(), w, org, uint(z), uint(y), uint(x)) + if err != nil { + log.Error().Err(err).Msg("failed to do tile") + http.Error(w, "failed to do tile", http.StatusInternalServerError) + return + } +} +func handleTile(ctx context.Context, w http.ResponseWriter, org *models.Organization, z, y, x uint) error { + if org.ArcgisMapServiceID.IsNull() { + return fmt.Errorf("no map service ID set") + } + map_service_id := org.ArcgisMapServiceID.MustGet() + tile_path := fmt.Sprintf("%s/tile-cache/%s/%d/%d/%d.raw", config.FilesDirectory, map_service_id, z, y, x) + file, err := os.Open(tile_path) + if err == nil { + defer file.Close() + img, err := io.ReadAll(file) + if err != nil { + return fmt.Errorf("readall from %s: %w", tile_path, err) + } + w.Header().Set("Content-Type", "image/png") + w.Header().Set("Content-Length", fmt.Sprintf("%d", len(img))) + _, err = io.Copy(w, bytes.NewBuffer(img)) + if err != nil { + return fmt.Errorf("copy bytes from %s: %w", tile_path) + } + return nil + } + content, err := imagetile.ImageAtTile(ctx, org, uint(z), uint(y), uint(x)) + if err != nil { + if errors.Is(err, imagetile.ErrNoTile) { + w.Header().Set("Content-Type", "image/png") + w.Header().Set("Content-Length", fmt.Sprintf("%d", len(content))) + _, err = io.Copy(w, bytes.NewBuffer(content)) + if err != nil { + return fmt.Errorf("write image file: %w", err) + } + return nil + } + return fmt.Errorf("image at tile: %w", err) + } + parent := filepath.Dir(tile_path) + err = os.MkdirAll(parent, 0750) + if err != nil { + return fmt.Errorf("mkdirall: %w", err) + } + err = os.WriteFile(tile_path, content, 0644) + if err != nil { + return fmt.Errorf("write image file: %w", err) + } + w.Header().Set("Content-Type", "image/png") + w.Header().Set("Content-Length", fmt.Sprintf("%d", len(content))) + _, err = io.Copy(w, bytes.NewBuffer(content)) + if err != nil { + return fmt.Errorf("write image file: %w", err) + } + return nil } diff --git a/html/static/js/map-proxied-arcgis-tile.js b/html/static/js/map-proxied-arcgis-tile.js new file mode 100644 index 00000000..ad965c46 --- /dev/null +++ b/html/static/js/map-proxied-arcgis-tile.js @@ -0,0 +1,153 @@ +// A map that shows multiple single point locations. +// Points have additional detail popups. +// The background layer is proxied from Arcgis +class MapProxiedArcgisTile extends HTMLElement { + static observedAttributes = ["latitude", "longitude"]; + constructor() { + super(); + + // Create a shadow DOM + this.attachShadow({ mode: "open" }); + + // Initial render + this.render(); + + // Keep track of any 'on' calls to add to the map as soon as we create it. + this._preOns = []; + this._map = null; + this._markers = []; + } + + attributeChangedCallback(name, old_value, new_value) { + //console.log("map-arcgis-tile: attribute changed", name, old_value, new_value); + if ((name == "latitude" || name == "longitude") && this._map != null) { + const latitude = parseFloat(this.getAttribute("latitude")); + const longitude = parseFloat(this.getAttribute("longitude")); + this._map.jumpTo({ + center: [longitude, latitude], + zoom: 19, + }); + } + } + + // Lifecycle: when element is added to the DOM + connectedCallback() { + // Initialize the map when the element is added to the DOM + setTimeout(() => this._initializeMap(), 0); + } + + disconnectedCallback() { + if (this._map) { + this._map.remove(); + } + } + + _initializeMap() { + const latitude = parseFloat(this.getAttribute("latitude")); + const longitude = parseFloat(this.getAttribute("longitude")); + const organization_id = Number(this.getAttribute("organization-id") || 0); + const tegola = this.getAttribute("tegola"); + const url_tiles = this.getAttribute("url-tiles"); + + const mapElement = this.shadowRoot.querySelector("#map"); + this._map = new maplibregl.Map({ + center: [longitude, latitude], + container: mapElement, + style: "https://tiles.stadiamaps.com/styles/osm_bright.json", + zoom: 19, + }); + this._map.on("load", () => { + if (organization_id != 0) { + this._map.addSource("tegola", { + type: "vector", + tiles: [ + `${tegola}maps/nidus/{z}/{x}/{y}?id=${organization_id}&organization_id=${organization_id}`, + ], + }); + this._map.addLayer({ + id: "service-area", + source: "tegola", + "source-layer": "service-area-bounds", + type: "line", + paint: { + "line-color": "#f00", + }, + }); + } + this._map.addSource("flyover", { + type: "raster", + tiles: [url_tiles], + }); + this._map.addLayer({ + id: "flyover-layer", + source: "flyover", + type: "raster", + }); + this.dispatchEvent(new CustomEvent("load"), { + bubbles: true, + composed: true, // Allows event to cross shadow DOM boundary + detail: { + map: this, + }, + }); + }); + for (const on of this._preOns) { + this._map.on(on.a, on.b); + } + } + + // Initial render of component + render() { + this.shadowRoot.innerHTML = ` + + +
+ `; + } + + addLayer(a) { + return this._map.addLayer(a); + } + addSource(a, b) { + return this._map.addSource(a, b); + } + jumpTo(args) { + return this._map.jumpTo(args); + } + on(a, b) { + if (this._map != null) { + return this._map.on(a, b); + } else { + this._preOns.push({ a: a, b: b }); + } + } + once(a, b) { + return this._map.once(a, b); + } + queryRenderedFeatures(a) { + return this._map.queryRenderedFeatures(a); + } + + FitBounds(bounds, options) { + return this._map.fitBounds(bounds, options); + } + SetLayoutProperty(layout, property, value) { + return this._map.setLayoutProperty(layout, property, value); + } + SetMarkers(markers) { + console.log("Setting map markers", markers); + this._markers.forEach((marker) => marker.remove()); + this._markers = markers; + for (let m of markers) { + m.addTo(this._map); + } + } +} + +customElements.define("map-proxied-arcgis-tile", MapProxiedArcgisTile); diff --git a/html/static/js/map-with-markers.js b/html/static/js/map-with-markers.js deleted file mode 100644 index 3225e2db..00000000 --- a/html/static/js/map-with-markers.js +++ /dev/null @@ -1,114 +0,0 @@ -var map = null; -// A map that just shows a bunch of markers, it can't change them -class MapWithMarkers extends HTMLElement { - constructor() { - super(); - - // Create a shadow DOM - this.attachShadow({mode: "open" }); - - // Initial render - this.render(); - - this._map = null; - - // markers shown on the map. Should be none or 1, generally. - this._markers = []; - } - - // Lifecycle: when element is added to the DOM - connectedCallback() { - // Initialize the map when the element is added to the DOM - setTimeout(() => this._initializeMap(), 0); - } - - disconnectedCallback() { - if (this._map) { - this._map.remove(); - } - } - - _initializeMap() { - console.log("Setting up the map..."); - const apiKey = this.getAttribute("api-key"); - const lat = Number(this.getAttribute('latitude') || 36.2); - const lng = Number(this.getAttribute('longitude') || -119.2); - const zoom = Number(this.getAttribute('zoom') || 15); - - mapboxgl.accessToken = apiKey; - const mapElement = this.shadowRoot.querySelector("#map"); - this._map = new mapboxgl.Map({ - container: mapElement, - center: { - lat: lat, - lng: lng, - }, - style: 'mapbox://styles/mapbox/streets-v12', // style URL - zoom: zoom, - }); - this._map.on("load", () => { - console.log("map loaded"); - this.dispatchEvent(new CustomEvent('load'), { - bubbles: true, - composed: true, // Allows event to cross shadow DOM boundary - detail: { - map: this - } - }); - }); - this._markers = []; - } - - // Initial render of component - render() { - this.shadowRoot.innerHTML = ` - - - -
-
-
- `; - } - - jumpTo(args) { - this._map.jumpTo(args); - } - - clearMarkers() { - this._markers.forEach((marker) => marker.remove()); - } - addMarker(coords, color) { - console.log("Add marker", coords, color); - const el = document.createElement("div"); - el.id = "marker"; - const marker = new mapboxgl.Marker({ - color: color, - scale: 1.5, - }).setLngLat(coords).addTo(this._map); - this._markers.push(marker); - } -} - -customElements.define('map-with-markers', MapWithMarkers); diff --git a/html/template/sync/review/pool.html b/html/template/sync/review/pool.html index b5f986d3..f91243cf 100644 --- a/html/template/sync/review/pool.html +++ b/html/template/sync/review/pool.html @@ -10,8 +10,7 @@ defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" > - - +