From d8818a095f39c06b760355c049d20ab937261fd9 Mon Sep 17 00:00:00 2001 From: Tariq Soliman Date: Mon, 24 Jul 2023 18:22:56 -0700 Subject: [PATCH 1/2] #403 - Bugfixes and rightclick finds features --- src/essence/Ancillary/ContextMenu.js | 2 + .../Basics/Layers_/LayerConstructors.js | 4 +- src/essence/Basics/Layers_/Layers_.js | 86 +++++++++++++++++++ src/essence/Basics/Map_/Map_.js | 18 +++- 4 files changed, 105 insertions(+), 5 deletions(-) diff --git a/src/essence/Ancillary/ContextMenu.js b/src/essence/Ancillary/ContextMenu.js index 9c2e439f..e6b553de 100644 --- a/src/essence/Ancillary/ContextMenu.js +++ b/src/essence/Ancillary/ContextMenu.js @@ -27,6 +27,8 @@ function showContextMenuMap(e) { [] ) + console.log(L_.getFeaturesAtPoint(e)) + hideContextMenuMap(true) var x = e.originalEvent.clientX var y = e.originalEvent.clientY diff --git a/src/essence/Basics/Layers_/LayerConstructors.js b/src/essence/Basics/Layers_/LayerConstructors.js index 5c7d9308..7fd14b98 100644 --- a/src/essence/Basics/Layers_/LayerConstructors.js +++ b/src/essence/Basics/Layers_/LayerConstructors.js @@ -67,10 +67,12 @@ export const constructVectorLayer = ( if (feature.properties.hasOwnProperty('style')) { let className = layerObj.style.className let layerName = layerObj.style.layerName + layerObj.style = Object.assign({}, layerObj.style) layerObj.style = { - ...JSON.parse(JSON.stringify(layerObj.style)), + ...layerObj.style, ...JSON.parse(JSON.stringify(feature.properties.style)), } + if (className) layerObj.style.className = className if (layerName) layerObj.style.layerName = layerName } else { diff --git a/src/essence/Basics/Layers_/Layers_.js b/src/essence/Basics/Layers_/Layers_.js index e3e7cf51..14904f12 100644 --- a/src/essence/Basics/Layers_/Layers_.js +++ b/src/essence/Basics/Layers_/Layers_.js @@ -2892,6 +2892,92 @@ const L_ = { } }) }, + // Returns all feature at a leaflet map click + // e = {latlng: {lat, lng}, containerPoint?: {x, y}} + getFeaturesAtPoint(e) { + let features = [] + if (e.latlng && e.latlng.lng != null && e.latlng.lat != null) { + // To better intersect points on click we're going to buffer out a small bounding box + const mapRect = document + .getElementById('map') + .getBoundingClientRect() + + const wOffset = e.containerPoint?.x || mapRect.width / 2 + const hOffset = e.containerPoint?.y || mapRect.height / 2 + + let nwLatLong = L_.Map_.map.containerPointToLatLng([ + wOffset - 15, + hOffset - 15, + ]) + let seLatLong = L_.Map_.map.containerPointToLatLng([ + wOffset + 15, + hOffset + 15, + ]) + // If we didn't have a container click point, buffer out e.latlng + if (e.containerPoint == null) { + const lngDif = Math.abs(nwLatLong.lng - seLatLong.lng) / 2 + const latDif = Math.abs(nwLatLong.lat - seLatLong.lat) / 2 + nwLatLong = { + lng: e.latlng.lng - lngDif, + lat: e.latlng.lat - latDif, + } + seLatLong = { + lng: e.latlng.lng + lngDif, + lat: e.latlng.lat + latDif, + } + } + + // Find all the intersected points and polygons of the click + Object.keys(L_.layers.layer).forEach((lName) => { + if ( + L_.layers.on[lName] && + L_.layers.data[lName].type === 'vector' && + L_.layers.layer[lName] + ) { + features = features.concat( + L.leafletPip + .pointInLayer( + [e.latlng.lng, e.latlng.lat], + L_.layers.layer[lName] + ) + .concat( + F_.pointsInPoint( + [e.latlng.lng, e.latlng.lat], + L_.layers.layer[lName], + [ + nwLatLong.lng, + seLatLong.lng, + nwLatLong.lat, + seLatLong.lat, + ] + ) + ) + .reverse() + ) + } + }) + + if (features[0] == null) features = [] + else { + const swapFeatures = [] + features.forEach((f) => { + if ( + typeof f.type === 'string' && + f.type.toLowerCase() === 'feature' + ) + swapFeatures.push(f) + else if ( + f.feature && + typeof f.feature.type === 'string' && + f.feature.type.toLowerCase() === 'feature' + ) + swapFeatures.push(f.feature) + }) + features = swapFeatures + } + } + return features + }, } //Takes in a configData object and does a depth-first search through its diff --git a/src/essence/Basics/Map_/Map_.js b/src/essence/Basics/Map_/Map_.js index cbde103e..a4e2c9b5 100644 --- a/src/essence/Basics/Map_/Map_.js +++ b/src/essence/Basics/Map_/Map_.js @@ -543,13 +543,23 @@ function getLayersChosenNamePropVal(feature, layer) { }) } } - // Use first key + + // Use first key that is not an object if (!foundThroughVariables) { for (let key in feature.properties) { - //Store the current feature's key - propertyNames = [key] + //Default to show geometry type + propertyNames = ['Type'] + propertyValues = [feature.geometry.type] + //Be certain we have that key in the feature - if (feature.properties.hasOwnProperty(key)) { + if ( + false && + feature.properties.hasOwnProperty(key) && + (typeof feature.properties[key] === 'string' || + typeof feature.properties[key] === 'number') + ) { + //Store the current feature's key + propertyNames = [key] //Store the current feature's value propertyValues = [feature.properties[key]] //Break out of for loop since we're done From 82c375622293c5d2f3a77a77d01edbd9001789a2 Mon Sep 17 00:00:00 2001 From: tariqksoliman Date: Tue, 25 Jul 2023 15:09:08 -0700 Subject: [PATCH 2/2] #403 ContextMenu, Actions on features, WKT link populate --- config/js/config.js | 5 + .../Tabs/Coordinates/Coordinates_Tab.md | 5 + package-lock.json | 11 +++ package.json | 1 + src/essence/Ancillary/ContextMenu.css | 36 ++++++- src/essence/Ancillary/ContextMenu.js | 70 ++++++++++++-- src/essence/Ancillary/Coordinates.js | 7 +- src/essence/Basics/Layers_/Layers_.js | 96 +++++++++++++++---- src/essence/Basics/Map_/Map_.js | 59 +----------- 9 files changed, 204 insertions(+), 86 deletions(-) diff --git a/config/js/config.js b/config/js/config.js index 6cee153c..4c753e3a 100644 --- a/config/js/config.js +++ b/config/js/config.js @@ -225,6 +225,11 @@ function initialize() { name: "The text for this menu entry when users right-click", link: "https://domain?I={ll[0]}&will={ll[1]}&replace={ll[2]}&these={en[0]}&brackets={en[1]}&for={cproj[0]}&you={sproj[0]}&with={rxy[0]}&coordinates={site[2]}", }, + { + name: "WKT text insertions. Do so only for polygons.", + link: "https://domain?regularWKT={wkt}&wkt_where_commas_are_replaced_with_underscores={wkt_}", + for: "polygon", + }, ], }, null, diff --git a/docs/pages/Configure/Tabs/Coordinates/Coordinates_Tab.md b/docs/pages/Configure/Tabs/Coordinates/Coordinates_Tab.md index 52de9324..f50a61c9 100644 --- a/docs/pages/Configure/Tabs/Coordinates/Coordinates_Tab.md +++ b/docs/pages/Configure/Tabs/Coordinates/Coordinates_Tab.md @@ -50,6 +50,11 @@ Example: { "name": "The text for this menu entry when users right-click", "link": "https://domain?I={ll[0]}&will={ll[1]}&replace={ll[2]}&these={en[0]}&brackets={en[1]}&for={cproj[0]}&you={sproj[0]}&with={rxy[0]}&coordinates={site[2]}" + }, + { + "name": "WKT text insertions. Do so only for polygons.", + "link": "https://domain?regularWKT={wkt}&wkt_where_commas_are_replaced_with_underscores={wkt_}", + "for": "polygon" } ] } diff --git a/package-lock.json b/package-lock.json index 3d246058..538f4a20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@eonasdan/tempus-dominus": "^6.2.6", "@popperjs/core": "^2.11.6", "@svgr/webpack": "4.3.3", + "@terraformer/wkt": "^2.2.0", "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.5.0", "@testing-library/user-event": "^7.2.1", @@ -2092,6 +2093,11 @@ "node": ">=8" } }, + "node_modules/@terraformer/wkt": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@terraformer/wkt/-/wkt-2.2.0.tgz", + "integrity": "sha512-i33rTSqPtmO4sRdeznI0IEc9gpIZZIXN5kGhZ4rTwVtDccDKL3h4uia9cmWdRJlJMlG4Febxatw5b9ylI5YYuA==" + }, "node_modules/@testing-library/dom": { "version": "6.16.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-6.16.0.tgz", @@ -23809,6 +23815,11 @@ "loader-utils": "^1.2.3" } }, + "@terraformer/wkt": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@terraformer/wkt/-/wkt-2.2.0.tgz", + "integrity": "sha512-i33rTSqPtmO4sRdeznI0IEc9gpIZZIXN5kGhZ4rTwVtDccDKL3h4uia9cmWdRJlJMlG4Febxatw5b9ylI5YYuA==" + }, "@testing-library/dom": { "version": "6.16.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-6.16.0.tgz", diff --git a/package.json b/package.json index 7b19e854..badc3654 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "@eonasdan/tempus-dominus": "^6.2.6", "@popperjs/core": "^2.11.6", "@svgr/webpack": "4.3.3", + "@terraformer/wkt": "^2.2.0", "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.5.0", "@testing-library/user-event": "^7.2.1", diff --git a/src/essence/Ancillary/ContextMenu.css b/src/essence/Ancillary/ContextMenu.css index 37ff69f7..fa933d64 100644 --- a/src/essence/Ancillary/ContextMenu.css +++ b/src/essence/Ancillary/ContextMenu.css @@ -2,11 +2,11 @@ position: absolute; background: var(--color-a); box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.3); - border: 1px solid var(--color-i); border-radius: 1px; font-size: 15px; z-index: 1; transition: opacity 0.2s cubic-bezier(0.39, 0.575, 0.565, 1); + overflow-y: auto; } .ContextMenuMap #contextMenuCursor { @@ -37,12 +37,13 @@ padding: 0; } .ContextMenuMap li { - padding: 5px 8px 5px 16px; + padding: 5px 16px 5px 16px; cursor: pointer; color: #aaa; border-top: 1px solid var(--color-a1); display: flex; transition: color 0.2s cubic-bezier(0.39, 0.575, 0.565, 1); + line-height: 18px; } .ContextMenuMap li:first-child { border-top: none; @@ -54,3 +55,34 @@ color: white; background: var(--color-a1); } + +.ContextMenuMap .contextMenuHeader { +} +.ContextMenuMap .contextMenuHeader span:nth-child(1) { + color: var(--color-h); + padding-right: 4px; + font-size: 12px; + line-height: 18px; +} +.ContextMenuMap .contextMenuHeader span:nth-child(2) { + color: var(--color-a5); + padding-right: 4px; +} +.ContextMenuMap .contextMenuHeader span:nth-child(3) { + color: var(--color-a4); + font-size: 12px; + line-height: 18px; + padding-left: 4px; +} +.ContextMenuMap .contextMenuHeader span:nth-child(4) { + padding-left: 4px; + color: var(--color-a7); +} + +.ContextMenuMap .contextMenuFeatureItem { + margin-left: 15px; + padding-left: 15px; + font-size: 13px; + line-height: 15px; + border-left: 1px solid var(--color-a1); +} \ No newline at end of file diff --git a/src/essence/Ancillary/ContextMenu.js b/src/essence/Ancillary/ContextMenu.js index e6b553de..5c7f8d0f 100644 --- a/src/essence/Ancillary/ContextMenu.js +++ b/src/essence/Ancillary/ContextMenu.js @@ -5,6 +5,8 @@ import F_ from '../Basics/Formulae_/Formulae_' import Map_ from '../Basics/Map_/Map_' import Coordinates from './Coordinates' +import { geojsonToWKT } from '@terraformer/wkt' + import './ContextMenu.css' var ContextMenu = { @@ -26,22 +28,56 @@ function showContextMenuMap(e) { 'configData.coordinates.variables.rightClickMenuActions', [] ) + const contextMenuActionsFull = [] + e.latlng = e.latlng || Coordinates.getLatLng(true) - console.log(L_.getFeaturesAtPoint(e)) + const featuresAtClick = L_.getFeaturesAtPoint(e, true) + featuresAtClick.splice(100) hideContextMenuMap(true) var x = e.originalEvent.clientX var y = e.originalEvent.clientY + // prettier-ignore var markup = [ - "
", + `
`, "
", "
", "
", "
", "
    ", "
  • Copy Coordinates
  • ", - contextMenuActions.map((a, idx) => `
  • ${a.name}${a.link != null ? `
    ` : ''}
  • ` ).join('\n'), + contextMenuActions.map((a, idx) => { + const items = [] + if(a.for == null) { + items.push(`
  • ${a.name}${a.link != null ? `
    ` : ''}
  • `) + contextMenuActionsFull.push({contextMenuAction: a, idx: idx, idx2: 0}) + } + return items.join('\n') + } ).join('\n'), + featuresAtClick.map((f, idx2) => { + const items = [] + const layerName = f.options.layerName + const displayName = L_.layers.data[layerName].display_name + const pv = L_.getLayersChosenNamePropVal(f.feature, layerName) + const key = Object.keys(pv)[0] + const val = pv[key] + items.push(`
  • ${f.feature.geometry.type}${displayName}-${key}:${val}
  • `) + contextMenuActionsFull.push({contextMenuAction: { goto: true }, idx: 'head', idx2: idx2, feature: f}) + contextMenuActions.map((a, idx) => { + const forLower = a.for ? a.for.toLowerCase() : null + switch(forLower) { + case "polygon": + if( f.feature.geometry.type.toLowerCase() === forLower) { + items.push(`
  • ${a.name}${a.link != null ? `
    ` : ''}
  • `) + contextMenuActionsFull.push({contextMenuAction: a, idx: idx, idx2: idx2, feature: f}) + } + break; + default: + } + } ) + return items.join('\n') + }).join('\n'), "
", "
" ].join('\n'); @@ -60,12 +96,14 @@ function showContextMenuMap(e) { }, 2000) }) - contextMenuActions.forEach((a, idx) => { - $(`#contextMenuAction_${idx}`).on('click', function () { + contextMenuActionsFull.forEach((c) => { + $(`#contextMenuAction_${c.idx}_${c.idx2}`).on('click', function () { + const a = c.contextMenuAction + const l = featuresAtClick[c.idx2] if (a.link) { let link = a.link - const lnglat = Coordinates.getLngLat() + const lnglat = Coordinates.getLngLat() Object.keys(Coordinates.states).forEach((s) => { if (link.indexOf(`{${s}[`) !== -1) { const converted = Coordinates.convertLngLat( @@ -89,8 +127,28 @@ function showContextMenuMap(e) { ) } }) + + let wkt + if (link.indexOf(`{wkt}`) !== -1) { + wkt = geojsonToWKT(l.feature.geometry) + link = link.replace(new RegExp(`{wkt}`, 'gi'), wkt) + } + if (link.indexOf(`{wkt_}`) !== -1) { + wkt = geojsonToWKT(l.feature.geometry) + link = link.replace( + new RegExp(`{wkt_}`, 'gi'), + wkt.replace(/,/g, '_') + ) + } window.open(link, '_blank').focus() } + if (a.goto === true) { + if (l) { + if (typeof l.getBounds === 'function') + Map_.map.fitBounds(l.getBounds()) + else if (l._latlng) Map_.map.panTo(l._latlng) + } + } }) }) } diff --git a/src/essence/Ancillary/Coordinates.js b/src/essence/Ancillary/Coordinates.js index 05526c15..d76684ea 100644 --- a/src/essence/Ancillary/Coordinates.js +++ b/src/essence/Ancillary/Coordinates.js @@ -328,7 +328,12 @@ const Coordinates = { getLngLat: function () { return Coordinates.mouseLngLat }, - getLatLng: function () { + getLatLng: function (asObject) { + if (asObject) + return { + lat: Coordinates.mouseLngLat[1], + lng: Coordinates.mouseLngLat[0], + } return [Coordinates.mouseLngLat[1], Coordinates.mouseLngLat[0]] }, getAllCoordinates: function () { diff --git a/src/essence/Basics/Layers_/Layers_.js b/src/essence/Basics/Layers_/Layers_.js index 14904f12..9246b7fb 100644 --- a/src/essence/Basics/Layers_/Layers_.js +++ b/src/essence/Basics/Layers_/Layers_.js @@ -2892,10 +2892,66 @@ const L_ = { } }) }, + //Specific internal functions likely only to be used once + getLayersChosenNamePropVal(feature, layer) { + //These are what you'd think they'd be (Name could be thought of as key) + let propertyNames, propertyValues + let foundThroughVariables = false + + let layerName = + typeof layer === 'string' ? layer : layer?.options?.layerName + if (layerName != null) { + const l = L_.layers.data[layerName] + if ( + l.hasOwnProperty('variables') && + l.variables.hasOwnProperty('useKeyAsName') + ) { + propertyNames = l.variables['useKeyAsName'] + if (typeof propertyNames === 'string') + propertyNames = [propertyNames] + propertyValues = Array(propertyNames.length).fill(null) + propertyNames.forEach((propertyName, idx) => { + if (feature.properties.hasOwnProperty(propertyName)) { + propertyValues[idx] = F_.getIn( + feature.properties, + propertyName + ) + if (propertyValues[idx] != null) + foundThroughVariables = true + } + }) + } + } + + // Use first key that is not an object + if (!foundThroughVariables) { + for (let key in feature.properties) { + //Default to show geometry type + propertyNames = ['Type'] + propertyValues = [feature.geometry.type] + + //Be certain we have that key in the feature + if ( + feature.properties.hasOwnProperty(key) && + (typeof feature.properties[key] === 'string' || + typeof feature.properties[key] === 'number') + ) { + //Store the current feature's key + propertyNames = [key] + //Store the current feature's value + propertyValues = [feature.properties[key]] + //Break out of for loop since we're done + break + } + } + } + return F_.stitchArrays(propertyNames, propertyValues) + }, // Returns all feature at a leaflet map click // e = {latlng: {lat, lng}, containerPoint?: {x, y}} - getFeaturesAtPoint(e) { + getFeaturesAtPoint(e, fullLayers) { let features = [] + let correspondingLayerNames = [] if (e.latlng && e.latlng.lng != null && e.latlng.lat != null) { // To better intersect points on click we're going to buffer out a small bounding box const mapRect = document @@ -2934,25 +2990,27 @@ const L_ = { L_.layers.data[lName].type === 'vector' && L_.layers.layer[lName] ) { - features = features.concat( - L.leafletPip - .pointInLayer( + const nextFeatures = L.leafletPip + .pointInLayer( + [e.latlng.lng, e.latlng.lat], + L_.layers.layer[lName] + ) + .concat( + F_.pointsInPoint( [e.latlng.lng, e.latlng.lat], - L_.layers.layer[lName] - ) - .concat( - F_.pointsInPoint( - [e.latlng.lng, e.latlng.lat], - L_.layers.layer[lName], - [ - nwLatLong.lng, - seLatLong.lng, - nwLatLong.lat, - seLatLong.lat, - ] - ) + L_.layers.layer[lName], + [ + nwLatLong.lng, + seLatLong.lng, + nwLatLong.lat, + seLatLong.lat, + ] ) - .reverse() + ) + .reverse() + features = features.concat(nextFeatures) + correspondingLayerNames = correspondingLayerNames.concat( + new Array(nextFeatures.length).fill().map(() => lName) ) } }) @@ -2971,7 +3029,7 @@ const L_ = { typeof f.feature.type === 'string' && f.feature.type.toLowerCase() === 'feature' ) - swapFeatures.push(f.feature) + swapFeatures.push(fullLayers ? f : f.feature) }) features = swapFeatures } diff --git a/src/essence/Basics/Map_/Map_.js b/src/essence/Basics/Map_/Map_.js index a4e2c9b5..12e0e1cb 100644 --- a/src/essence/Basics/Map_/Map_.js +++ b/src/essence/Basics/Map_/Map_.js @@ -513,63 +513,6 @@ let Map_ = { allLayersLoaded: allLayersLoaded, } -//Specific internal functions likely only to be used once -function getLayersChosenNamePropVal(feature, layer) { - //These are what you'd think they'd be (Name could be thought of as key) - let propertyNames, propertyValues - let foundThroughVariables = false - if ( - layer.hasOwnProperty('options') && - layer.options.hasOwnProperty('layerName') - ) { - const l = L_.layers.data[layer.options.layerName] - if ( - l.hasOwnProperty('variables') && - l.variables.hasOwnProperty('useKeyAsName') - ) { - propertyNames = l.variables['useKeyAsName'] - if (typeof propertyNames === 'string') - propertyNames = [propertyNames] - propertyValues = Array(propertyNames.length).fill(null) - propertyNames.forEach((propertyName, idx) => { - if (feature.properties.hasOwnProperty(propertyName)) { - propertyValues[idx] = F_.getIn( - feature.properties, - propertyName - ) - if (propertyValues[idx] != null) - foundThroughVariables = true - } - }) - } - } - - // Use first key that is not an object - if (!foundThroughVariables) { - for (let key in feature.properties) { - //Default to show geometry type - propertyNames = ['Type'] - propertyValues = [feature.geometry.type] - - //Be certain we have that key in the feature - if ( - false && - feature.properties.hasOwnProperty(key) && - (typeof feature.properties[key] === 'string' || - typeof feature.properties[key] === 'number') - ) { - //Store the current feature's key - propertyNames = [key] - //Store the current feature's value - propertyValues = [feature.properties[key]] - //Break out of for loop since we're done - break - } - } - } - return F_.stitchArrays(propertyNames, propertyValues) -} - //Takes an array of layer objects and makes them map layers function makeLayers(layersObj) { //Make each layer (backwards to maintain draw order) @@ -611,7 +554,7 @@ async function makeLayer(layerObj, evenIfOff, forceGeoJSON) { //Default is onclick show full properties and onhover show 1st property Map_.onEachFeatureDefault = onEachFeatureDefault function onEachFeatureDefault(feature, layer) { - const pv = getLayersChosenNamePropVal(feature, layer) + const pv = L_.getLayersChosenNamePropVal(feature, layer) layer['useKeyAsName'] = Object.keys(pv)[0] if (