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..6329353d 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: colors.black }), + radius: 4, + }), + }) }; 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..161f0848 100644 --- a/ohmg/frontend/svelte_components/src/lib/viewers.js +++ b/ohmg/frontend/svelte_components/src/lib/viewers.js @@ -1,13 +1,24 @@ 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 { toGeometry } from 'ol/render/Feature'; + +import { containsXY, intersects } from 'ol/extent'; + +import { snapVertexStyle } from '../lib/ol-styles'; import { makeBasemaps } from './utils'; export class MapViewer { interactions = {}; currentBasemap = null; + currentVectorTileExtents = []; + snapCandidates = [] constructor(elementId, maxTilesLoading) { if (!maxTilesLoading) { @@ -109,4 +120,130 @@ 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 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(); + 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) + } + } + }) + } + } + + 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) + } + } + }); + } }