From 7d644d4c7ca13b435a33edf13ee6aee3fdd9f84e Mon Sep 17 00:00:00 2001 From: Adam Cox Date: Thu, 12 Feb 2026 12:47:56 -0600 Subject: [PATCH 1/3] refactor parcel snap, improve styles --- .../src/components/Georeferencer.svelte | 125 +++--------------- .../svelte_components/src/lib/ol-styles.js | 14 +- .../svelte_components/src/lib/viewers.js | 105 ++++++++++++++- 3 files changed, 139 insertions(+), 105 deletions(-) diff --git a/ohmg/frontend/svelte_components/src/components/Georeferencer.svelte b/ohmg/frontend/svelte_components/src/components/Georeferencer.svelte index 83106e45..b63bf8db 100644 --- a/ohmg/frontend/svelte_components/src/components/Georeferencer.svelte +++ b/ohmg/frontend/svelte_components/src/components/Georeferencer.svelte @@ -23,11 +23,9 @@ import { transformExtent } from 'ol/proj'; import { containsXY } from 'ol/extent'; - import { inflateCoordinatesArray } from 'ol/geom/flat/inflate'; import Draw from 'ol/interaction/Draw'; import Modify from 'ol/interaction/Modify'; - import Snap from 'ol/interaction/Snap'; import { gcpStyles, parcelStyles, emptyStyle } from '../lib/ol-styles'; import { @@ -93,8 +91,6 @@ let currentBasemap; let currentZoom; - let enableSnapLayer = false; - let currentPreviewId; let defaultExtent; @@ -201,7 +197,7 @@ const mapGCPLayer = new VectorLayer({ source: mapGCPSource, style: gcpStyles.default, - zIndex: 30, + zIndex: 100, }); // CREATE DISPLAY LAYERS @@ -302,28 +298,9 @@ } // SNAP LAYER STUFF - let pmLayer; + let parcelLayer; let parcelEntry; let localeMatch; - MAP.locale_lineage.forEach((slug) => { - if (parcelLookup[slug]) { - localeMatch = slug; - parcelEntry = parcelLookup[slug]; - pmLayer = makePmTilesLayer( - parcelEntry.pmtilesUrl, - `${parcelEntry.attributionText}`, - parcelStyles.inactive, - ); - return; - } - }); - const snapSource = new VectorSource({ - overlaps: false, - }); - const snapLayer = new VectorLayer({ - source: snapSource, - style: emptyStyle, - }); // MAKING INTERACTIONS @@ -397,6 +374,7 @@ return containsXY(docExtent, mapBrowserEvent.coordinate[0], mapBrowserEvent.coordinate[1]); } + docViewer.addInteraction('draw', makeDrawInteraction(docGCPSource, drawWithinDocCondition, emptyStyle)); docViewer.addInteraction('modify', makeModifyInteraction(docGCPSource, docViewer.element)); @@ -420,8 +398,10 @@ mapViewer.addControl(new MapScaleLine()); // create interactions - const mapDrawGCPStyle = pmLayer ? gcpStyles.snapTarget : emptyStyle; - mapViewer.addInteraction('draw', makeDrawInteraction(mapGCPSource, null, mapDrawGCPStyle)); + function drawStyleFunction() { + return parcelLayer.getVisible() ? gcpStyles.snapTarget : emptyStyle + } + mapViewer.addInteraction('draw', makeDrawInteraction(mapGCPSource, null, drawStyleFunction)); mapViewer.addInteraction('modify', makeModifyInteraction(mapGCPSource, mapViewer.element)); // add some event listening to the map @@ -438,17 +418,20 @@ mapViewer.addLayer(mainLayerGroup); mapViewer.addLayer(mainLayerGroup50); - // snap to parcels --- work-in-progress! - const snap = new Snap({ - source: snapSource, - edge: false, + MAP.locale_lineage.forEach((slug) => { + if (parcelLookup[slug]) { + localeMatch = slug + parcelLayer = makePmTilesLayer( + parcelLookup[slug].pmtilesUrl, + `${parcelLookup[slug].attributionText}`, + parcelStyles.inactive, + ); + parcelLayer.setZIndex(30) + parcelLayer.setVisible(false) + mapViewer.addSnappableVectorLayer(parcelLayer, 10, 17, parcelStyles.active, parcelStyles.inactive) + return; + } }); - mapViewer.addInteraction('parcelSnap', snap); - mapViewer.interactions.parcelSnap.setActive(false); - // tried map.on('rendercomplete') here but sometimes it would fire constantly, - // so using these more specific event listeners - mapViewer.map.getView().on('change:resolution', refreshSnapSource); - mapViewer.map.on('moveend', refreshSnapSource); currentZoom = mapViewer.getZoom(); mapViewer.map.getView().on('change:resolution', () => { @@ -620,70 +603,6 @@ } } - $: { - if (currentZoom < 17) { - enableParcelSnapping = false; - } - } - - $: enableParcelSnapping = currentZoom >= 17; - - function refreshSnapSource() { - if (!enableParcelSnapping || !pmLayer) { - return; - } - snapSource.clear(); - const features = pmLayer.getFeaturesInExtent(mapViewer.map.getView().calculateExtent()); - features.forEach(function (feature) { - const lineCoords = inflateCoordinatesArray( - feature.getFlatCoordinates(), // flat coordinates - 0, // offset - feature.getEnds(), // geometry end indices - 2, // stride - ); - const geoJsonGeom = { coordinates: lineCoords, type: 'Polygon' }; - const f = new GeoJSON().readFeature(geoJsonGeom, { - dataProjection: 'EPSG:3857', - }); - if (!snapSource.hasFeature(f)) { - snapSource.addFeature(f); - } - }); - } - function toggleSnap(enabled) { - if (!mapViewer || !pmLayer) { - return; - } - if (enabled) { - mapViewer.interactions.parcelSnap.setActive(true); - mapViewer.map.once('rendercomplete', refreshSnapSource); - pmLayer.setStyle(parcelStyles.active); - } else { - snapSource.clear(); - mapViewer.interactions.parcelSnap.setActive(false); - pmLayer.setStyle(parcelStyles.inactive); - } - } - $: toggleSnap(enableParcelSnapping); - - function toggleParcelLayer(enabled) { - if (!mapViewer || !pmLayer) { - return; - } - if (currentZoom < 10) { - enableParcelSnapping = false; - } - if (enabled) { - mapViewer.addLayer(snapLayer); - mapViewer.addLayer(pmLayer); - } else { - snapSource.clear(); - mapViewer.map.removeLayer(snapLayer); - mapViewer.map.removeLayer(pmLayer); - } - } - $: toggleParcelLayer(enableSnapLayer); - function setPreviewVisibility(mode) { if (!mapViewer) { return; @@ -1077,11 +996,11 @@ {/each} - {#if pmLayer} + {#if parcelLayer}
diff --git a/ohmg/frontend/svelte_components/src/lib/ol-styles.js b/ohmg/frontend/svelte_components/src/lib/ol-styles.js index c6d6b654..2d3394e5 100644 --- a/ohmg/frontend/svelte_components/src/lib/ol-styles.js +++ b/ohmg/frontend/svelte_components/src/lib/ol-styles.js @@ -105,7 +105,12 @@ export const gcpStyles = { default: [gcpOutline, makeGcpX(gcpColors.default)], hover: [gcpOutline, makeGcpX(gcpColors.hover)], selected: [gcpOutline, makeGcpX(gcpColors.selected)], - snapTarget: smallCross, + snapTarget: new Style({ + image: new Circle({ + fill: new Fill({ color: 'red' }), + radius: 3, + }), + }) }; export const parcelStyles = { @@ -123,6 +128,13 @@ export const parcelStyles = { }), }; +export const snapVertexStyle = new Style({ + image: new Circle({ + stroke: new Stroke({ color: colors.black, width: 1 }), + radius: 4, + }), +}); + export const emptyStyle = new Style(); // SPLIT INTERFACE diff --git a/ohmg/frontend/svelte_components/src/lib/viewers.js b/ohmg/frontend/svelte_components/src/lib/viewers.js index 4d57b6c2..89ceb45b 100644 --- a/ohmg/frontend/svelte_components/src/lib/viewers.js +++ b/ohmg/frontend/svelte_components/src/lib/viewers.js @@ -1,7 +1,15 @@ import Map from 'ol/Map'; import ZoomToExtent from 'ol/control/ZoomToExtent'; -import Draw from 'ol/interaction/Draw'; +import Snap from 'ol/interaction/Snap'; import Link from 'ol/interaction/Link.js'; +import VectorSource from 'ol/source/Vector'; +import VectorLayer from 'ol/layer/Vector'; +import GeoJSON from 'ol/format/GeoJSON'; +import { inflateCoordinatesArray } from 'ol/geom/flat/inflate'; + +import { containsXY } from 'ol/extent'; + +import { snapVertexStyle } from '../lib/ol-styles'; import { makeBasemaps } from './utils'; @@ -109,4 +117,99 @@ export class MapViewer { getZoom() { return Math.round(this.map.getView().getZoom() * 10) / 10; } + + addSnappableVectorLayer(layer, visMinZoom, snapMinZoom, activeStyle, inactiveStyle) { + + const snapSource = new VectorSource({ + overlaps: false, + }); + const snapLayer = new VectorLayer({ + source: snapSource, + zIndex: layer.get("zIndex") + 1, + style: snapVertexStyle, + }); + + const snap = new Snap({ + source: snapSource, + }); + this.addInteraction('parcelSnap', snap); + + const refreshSnapSource = () => { + snapSource.clear(); + // const usedX = []; + // const usedY = []; + const mapExtent = this.map.getView().calculateExtent() + if (this.getZoom() >= snapMinZoom) { + const features = layer.getFeaturesInExtent(mapExtent); + features.forEach((feature) => { + // IDEA: used these later to check and exclude exact corner coords + // (i.e. corners of the actual vector tiles) + // ref: https://github.com/openlayers/openlayers/issues/17328 + // const [minX, minY, maxX, maxY] = feature.getExtent() + // const cornerCoords = [[minX, minY], [maxX, minY], [minX, maxY], [maxX, maxY]] + const coordsArray = inflateCoordinatesArray( + feature.getFlatCoordinates(), // flat coordinates + 0, // offset + feature.getEnds(), // geometry end indices + 2, // stride + ); + coordsArray.forEach((lineCoords) => { + lineCoords.forEach((coord) => { + if (!usedX.includes[coord[0]] && !usedY.includes[coord[1]]) { + if (containsXY(mapExtent, coord[0], coord[1])) { + const geoJsonGeom = { coordinates: coord, type: 'Point' }; + const pnt = new GeoJSON().readFeature(geoJsonGeom, { + dataProjection: 'EPSG:3857', + }); + // This doesn't work but it would be nice to fix it so that + // each coordinate really is only added once time. + if (!snapSource.hasFeature(pnt)) { + snapSource.addFeature(pnt); + } + // this also doesn't work... + // usedX.push(coord[0]) + // usedY.push(coord[1]) + } + } + }); + }); + }); + } + } + + this.map.on('moveend', () => { + refreshSnapSource() + }); + + layer.on('change:visible', () => { + if (layer.getVisible()) { + layer.once('postrender', () => { + setTimeout(refreshSnapSource, 500) + }) + } else { + snapSource.clear() + } + }) + + this.map.getView().on('change:resolution', () => { + const currentZoom = this.getZoom(); + // first handle the presence of this layer at all + if (currentZoom < visMinZoom) { + this.map.removeLayer(layer) + } else { + if (!this.map.getLayers().getArray().includes(layer)) { + this.map.addLayer(layer) + } + } + // now handle style based on whether it is snappable or not + if (currentZoom < snapMinZoom) { + layer.setStyle(inactiveStyle) + } else { + layer.setStyle(activeStyle) + if (!this.map.getLayers().getArray().includes(snapLayer)) { + this.map.addLayer(snapLayer) + } + } + }); + } } From 444c6958dcc9657963519b43a19c18fb7f4f36b9 Mon Sep 17 00:00:00 2001 From: Adam Cox Date: Thu, 12 Feb 2026 13:06:00 -0600 Subject: [PATCH 2/3] cleanup duplicate coords, update style --- .../svelte_components/src/lib/ol-styles.js | 2 +- .../svelte_components/src/lib/viewers.js | 21 +++++++------------ 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/ohmg/frontend/svelte_components/src/lib/ol-styles.js b/ohmg/frontend/svelte_components/src/lib/ol-styles.js index 2d3394e5..ddf17787 100644 --- a/ohmg/frontend/svelte_components/src/lib/ol-styles.js +++ b/ohmg/frontend/svelte_components/src/lib/ol-styles.js @@ -107,7 +107,7 @@ export const gcpStyles = { selected: [gcpOutline, makeGcpX(gcpColors.selected)], snapTarget: new Style({ image: new Circle({ - fill: new Fill({ color: 'red' }), + fill: new Fill({ color: colors.white }), radius: 3, }), }) diff --git a/ohmg/frontend/svelte_components/src/lib/viewers.js b/ohmg/frontend/svelte_components/src/lib/viewers.js index 89ceb45b..8d57c411 100644 --- a/ohmg/frontend/svelte_components/src/lib/viewers.js +++ b/ohmg/frontend/svelte_components/src/lib/viewers.js @@ -136,8 +136,7 @@ export class MapViewer { const refreshSnapSource = () => { snapSource.clear(); - // const usedX = []; - // const usedY = []; + const usedCoords = [] const mapExtent = this.map.getView().calculateExtent() if (this.getZoom() >= snapMinZoom) { const features = layer.getFeaturesInExtent(mapExtent); @@ -155,20 +154,16 @@ export class MapViewer { ); coordsArray.forEach((lineCoords) => { lineCoords.forEach((coord) => { - if (!usedX.includes[coord[0]] && !usedY.includes[coord[1]]) { - if (containsXY(mapExtent, coord[0], coord[1])) { - const geoJsonGeom = { coordinates: coord, type: 'Point' }; + const coordRnd = [parseFloat(coord[0].toFixed(6)), parseFloat(coord[1].toFixed(6))] + const coordStr = coordRnd.toString() + if (!usedCoords.includes(coordStr)) { + if (containsXY(mapExtent, coordRnd[0], coordRnd[1])) { + const geoJsonGeom = { coordinates: coordRnd, type: 'Point' }; const pnt = new GeoJSON().readFeature(geoJsonGeom, { dataProjection: 'EPSG:3857', }); - // This doesn't work but it would be nice to fix it so that - // each coordinate really is only added once time. - if (!snapSource.hasFeature(pnt)) { - snapSource.addFeature(pnt); - } - // this also doesn't work... - // usedX.push(coord[0]) - // usedY.push(coord[1]) + snapSource.addFeature(pnt); + usedCoords.push(coordStr) } } }); From 7017bac8f2eff164f9b134c301cfde73f6a6e258 Mon Sep 17 00:00:00 2001 From: Adam Cox Date: Fri, 13 Feb 2026 16:11:29 -0600 Subject: [PATCH 3/3] load snap points on tile basis, trim points outside of tile --- .../svelte_components/src/lib/ol-styles.js | 4 +- .../svelte_components/src/lib/viewers.js | 107 ++++++++++++------ 2 files changed, 75 insertions(+), 36 deletions(-) diff --git a/ohmg/frontend/svelte_components/src/lib/ol-styles.js b/ohmg/frontend/svelte_components/src/lib/ol-styles.js index ddf17787..6329353d 100644 --- a/ohmg/frontend/svelte_components/src/lib/ol-styles.js +++ b/ohmg/frontend/svelte_components/src/lib/ol-styles.js @@ -107,8 +107,8 @@ export const gcpStyles = { selected: [gcpOutline, makeGcpX(gcpColors.selected)], snapTarget: new Style({ image: new Circle({ - fill: new Fill({ color: colors.white }), - radius: 3, + fill: new Fill({ color: colors.black }), + radius: 4, }), }) }; diff --git a/ohmg/frontend/svelte_components/src/lib/viewers.js b/ohmg/frontend/svelte_components/src/lib/viewers.js index 8d57c411..161f0848 100644 --- a/ohmg/frontend/svelte_components/src/lib/viewers.js +++ b/ohmg/frontend/svelte_components/src/lib/viewers.js @@ -5,9 +5,10 @@ import Link from 'ol/interaction/Link.js'; import VectorSource from 'ol/source/Vector'; import VectorLayer from 'ol/layer/Vector'; import GeoJSON from 'ol/format/GeoJSON'; -import { inflateCoordinatesArray } from 'ol/geom/flat/inflate'; -import { containsXY } from 'ol/extent'; +import { toGeometry } from 'ol/render/Feature'; + +import { containsXY, intersects } from 'ol/extent'; import { snapVertexStyle } from '../lib/ol-styles'; @@ -16,6 +17,8 @@ import { makeBasemaps } from './utils'; export class MapViewer { interactions = {}; currentBasemap = null; + currentVectorTileExtents = []; + snapCandidates = [] constructor(elementId, maxTilesLoading) { if (!maxTilesLoading) { @@ -134,41 +137,77 @@ export class MapViewer { }); this.addInteraction('parcelSnap', snap); + const collapseNestedCoordinates = (array, outCoords) => { + if (!outCoords) { outCoords = [] } + // if the first item in the list is not an array, then this is a coord + if (!Array.isArray(array[0])) { + outCoords.push(array) + } else { + array.forEach((a) => collapseNestedCoordinates(a, outCoords)) + } + return outCoords + } + + // this.map.on("moveend", () => { + // getTilesInCurrentView() + // }) + + const self = this; + function getTilesInCurrentView() { + const zoom = Math.floor(self.getZoom()); // Get current integer zoom level + const tileGrid = layer.getSource().getTileGrid(); // Get the tile grid from the source + // console.log(extent) + // console.log(tileGrid) + const tiles = []; + + const [minX, minY, maxX, maxY] = self.map.getView().calculateExtent(self.map.getSize()) + const cornerCoords = [[minX, minY], [maxX, minY], [minX, maxY], [maxX, maxY]] + cornerCoords.forEach((coord) => { + const tc = tileGrid.getTileCoordForCoordAndZ([coord[0], coord[1]], zoom) + console.log(tc) + const tile = layer.getSource().getTile(tc[0], tc[1], tc[2], 1, layer.getSource().getProjection()) + + }); + // const blTc = tileGrid.getTileCoordForCoordAndZ([extent[0], extent[1]], zoom) + // const tlTc = tileGrid.getTileCoordForCoordAndZ([extent[0], extent[1]], zoom) + + // const tileCoord = tileGrid.getTileCoordForCoordAndZ([extent[0], extent[1]], zoom) + // console.log(tile) + + + console.log(`Tiles intersecting view at zoom ${zoom}:`, tiles); + return tiles; + } + + layer.getSource().on("tileloadend", (evt) => { + evt.tile.getFeatures().forEach((f) => { + const featCoords = collapseNestedCoordinates(toGeometry(f).getCoordinates()); + const featCoordsInTileExtent = featCoords.filter(i => containsXY(evt.tile.extent, i[0], i[1])) + self.snapCandidates.push(...featCoordsInTileExtent) + }); + }) + const refreshSnapSource = () => { snapSource.clear(); - const usedCoords = [] - const mapExtent = this.map.getView().calculateExtent() - if (this.getZoom() >= snapMinZoom) { - const features = layer.getFeaturesInExtent(mapExtent); - features.forEach((feature) => { - // IDEA: used these later to check and exclude exact corner coords - // (i.e. corners of the actual vector tiles) - // ref: https://github.com/openlayers/openlayers/issues/17328 - // const [minX, minY, maxX, maxY] = feature.getExtent() - // const cornerCoords = [[minX, minY], [maxX, minY], [minX, maxY], [maxX, maxY]] - const coordsArray = inflateCoordinatesArray( - feature.getFlatCoordinates(), // flat coordinates - 0, // offset - feature.getEnds(), // geometry end indices - 2, // stride - ); - coordsArray.forEach((lineCoords) => { - lineCoords.forEach((coord) => { - const coordRnd = [parseFloat(coord[0].toFixed(6)), parseFloat(coord[1].toFixed(6))] - const coordStr = coordRnd.toString() - if (!usedCoords.includes(coordStr)) { - if (containsXY(mapExtent, coordRnd[0], coordRnd[1])) { - const geoJsonGeom = { coordinates: coordRnd, type: 'Point' }; - const pnt = new GeoJSON().readFeature(geoJsonGeom, { - dataProjection: 'EPSG:3857', - }); - snapSource.addFeature(pnt); - usedCoords.push(coordStr) - } + if (this.getZoom() >= snapMinZoom && layer.getVisible()) { + + const currentExtent = this.map.getView().calculateExtent() + + const usedCoords = [] + this.snapCandidates.forEach((coord) => { + const coordRnd = [parseFloat(coord[0].toFixed(6)), parseFloat(coord[1].toFixed(6))] + const coordStr = coordRnd.toString() + if (!usedCoords.includes(coordStr)) { + if (containsXY(currentExtent, coord[0], coord[1])) { + const geoJsonGeom = { coordinates: coordRnd, type: 'Point' }; + const pnt = new GeoJSON().readFeature(geoJsonGeom, { + dataProjection: 'EPSG:3857', + }); + snapSource.addFeature(pnt); + usedCoords.push(coordStr) } - }); - }); - }); + } + }) } }