From a1a43891be60928cc082e62ff211f926d8d6acee Mon Sep 17 00:00:00 2001 From: Jeff Jacobson Date: Wed, 7 Aug 2024 14:47:23 -0700 Subject: [PATCH 1/6] refactor: :recycle: Moved some code into functions --- src/layers/MilepostLayer/index.ts | 154 ++++++++++++++++++------------ 1 file changed, 95 insertions(+), 59 deletions(-) diff --git a/src/layers/MilepostLayer/index.ts b/src/layers/MilepostLayer/index.ts index ddb8a1be..6a4df122 100644 --- a/src/layers/MilepostLayer/index.ts +++ b/src/layers/MilepostLayer/index.ts @@ -1,5 +1,5 @@ import { objectIdFieldName } from "../../elc/types"; -import arcadeExpressions from "./arcade"; +import type { MilepostExpressionInfo } from "./arcade"; import type SpatialReference from "@arcgis/core/geometry/SpatialReference"; import type Field from "@arcgis/core/layers/support/Field"; @@ -10,9 +10,6 @@ export const enum fieldNames { Srmp = "Srmp", Back = "Back", Direction = "Direction", - // TownshipSubdivision = "Township Subdivision", - // County = "County", - // City = "City", } const fields = [ @@ -67,39 +64,78 @@ export async function createMilepostLayer(spatialReference: SpatialReference) { const [ { default: FeatureLayer }, { default: FieldInfo }, - { default: SimpleRenderer }, { default: waExtent }, { default: labelClass }, - { highwaySignBackgroundColor, highwaySignTextColor }, ] = await Promise.all([ import("@arcgis/core/layers/FeatureLayer"), import("@arcgis/core/popup/FieldInfo"), - import("@arcgis/core/renderers/SimpleRenderer"), import("../../WAExtent"), import("./labelClass"), - import("../../colors"), ]); + /** - * This is the symbol for the point on the route. + * A function that creates and adds field information for an expression. + * @param milepostExpressionInfo - The expression information to create the field info for. + * @returns The created field info. */ - const actualMPSymbol = await import( - "@arcgis/core/symbols/SimpleMarkerSymbol" - ).then( - ({ default: SimpleMarkerSymbol }) => - new SimpleMarkerSymbol({ - color: highwaySignBackgroundColor, - size: 12, - style: "circle", - outline: { - width: 1, - color: highwaySignTextColor, - }, - }), - ); + function createAndAddFieldInfoForExpression( + milepostExpressionInfo: MilepostExpressionInfo, + ) { + const fieldInfo = new FieldInfo({ + fieldName: `expression/${milepostExpressionInfo.name}`, + visible: !["webMercatorToWgs1984", "milepostLabel"].includes( + milepostExpressionInfo.name, + ), + }); + return fieldInfo; + } - const renderer = new SimpleRenderer({ - symbol: actualMPSymbol, - }); + /** + * Creates a popup template for the milepost layer by hiding certain fields and adding arcade expressions. + * @returns The created popup template. + */ + function createPopupTemplate() { + const popupTemplate = milepostLayer.createPopupTemplate(); + + // Set certain fields to be hidden in the popup. + for (const element of [ + fieldNames.Route, + fieldNames.Srmp, + fieldNames.Back, + fieldNames.Direction, + ]) { + const fieldInfo = popupTemplate.fieldInfos.find( + (fi) => fi.fieldName === (element as string), + ); + + if (fieldInfo) { + fieldInfo.visible = false; + } + } + + // Import the Arcade expressions, add them to the popup template, and then + // add them to the popup template's fieldInfos array. + import("./arcade") + .then(({ default: arcadeExpressions }) => { + popupTemplate.expressionInfos = arcadeExpressions; + + // Append expressions to the PopupTemplate's fieldInfos array. + for (const xi of arcadeExpressions) { + const fieldInfo = createAndAddFieldInfoForExpression(xi); + popupTemplate.fieldInfos.push(fieldInfo); + } + popupTemplate.title = + "{Route} ({Direction}) @ {expression/milepostLabel}"; + }) + .catch((error: unknown) => { + console.error("Error adding Arcade expressions", error); + }); + + return popupTemplate; + } + /** + * This is the symbol for the point on the route. + */ const milepostLayer = new FeatureLayer({ labelingInfo: [labelClass], title: "Mileposts", @@ -109,7 +145,6 @@ export async function createMilepostLayer(spatialReference: SpatialReference) { geometryType: "point", objectIdField: objectIdFieldName, fullExtent: waExtent, - renderer, spatialReference, // Since there are no features at the beginning, // need to add an empty array as the source. @@ -118,39 +153,40 @@ export async function createMilepostLayer(spatialReference: SpatialReference) { hasM: true, }); - const popupTemplate = milepostLayer.createPopupTemplate(); + createRenderer() + .then((renderer) => { + milepostLayer.renderer = renderer; + }) + .catch((error: unknown) => { + console.error("Error creating renderer", error); + }); - // Set certain fields to be hidden in the popup. - for (const element of [ - fieldNames.Route, - fieldNames.Srmp, - fieldNames.Back, - fieldNames.Direction, - ]) { - const fieldInfo = popupTemplate.fieldInfos.find( - (fi) => fi.fieldName === (element as string), - ); - - if (fieldInfo) { - fieldInfo.visible = false; - } - } - - popupTemplate.expressionInfos = arcadeExpressions; - - // Append expressions to the PopupTemplate's fieldInfos array. - for (const xi of arcadeExpressions) { - popupTemplate.fieldInfos.push( - new FieldInfo({ - fieldName: `expression/${xi.name}`, - visible: !["webMercatorToWgs1984", "milepostLabel"].includes(xi.name), - }), - ); - } - - milepostLayer.popupTemplate = popupTemplate; - - popupTemplate.title = "{Route} ({Direction}) @ {expression/milepostLabel}"; + milepostLayer.popupTemplate = createPopupTemplate(); return milepostLayer; } +async function createRenderer() { + const [ + { default: SimpleMarkerSymbol }, + { default: SimpleRenderer }, + { highwaySignBackgroundColor, highwaySignTextColor }, + ] = await Promise.all([ + import("@arcgis/core/symbols/SimpleMarkerSymbol"), + import("@arcgis/core/renderers/SimpleRenderer"), + import("../../colors"), + ]); + const actualMPSymbol = new SimpleMarkerSymbol({ + color: highwaySignBackgroundColor, + size: 12, + style: "circle", + outline: { + width: 1, + color: highwaySignTextColor, + }, + }); + + const renderer = new SimpleRenderer({ + symbol: actualMPSymbol, + }); + return renderer; +} From dd9d67e271f056041f92af3eb7b2df34ec64bf06 Mon Sep 17 00:00:00 2001 From: Jeff Jacobson Date: Wed, 7 Aug 2024 15:23:41 -0700 Subject: [PATCH 2/6] refactor: :recycle: Simplified hiding of fields in popup. --- src/layers/MilepostLayer/index.ts | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/src/layers/MilepostLayer/index.ts b/src/layers/MilepostLayer/index.ts index 6a4df122..9b481971 100644 --- a/src/layers/MilepostLayer/index.ts +++ b/src/layers/MilepostLayer/index.ts @@ -95,23 +95,11 @@ export async function createMilepostLayer(spatialReference: SpatialReference) { * @returns The created popup template. */ function createPopupTemplate() { - const popupTemplate = milepostLayer.createPopupTemplate(); - - // Set certain fields to be hidden in the popup. - for (const element of [ - fieldNames.Route, - fieldNames.Srmp, - fieldNames.Back, - fieldNames.Direction, - ]) { - const fieldInfo = popupTemplate.fieldInfos.find( - (fi) => fi.fieldName === (element as string), - ); - - if (fieldInfo) { - fieldInfo.visible = false; - } - } + const popupTemplate = milepostLayer.createPopupTemplate({ + // Hide all of the initial fields. + // These fields are already displayed in the popup's title. + visibleFieldNames: new Set(), + }); // Import the Arcade expressions, add them to the popup template, and then // add them to the popup template's fieldInfos array. From 5b31d1a198a6947da7c441a053631c4677f0cca1 Mon Sep 17 00:00:00 2001 From: Jeff Jacobson Date: Wed, 7 Aug 2024 17:02:15 -0700 Subject: [PATCH 3/6] refactor: :mute: Removed Console calls in Arcade --- src/layers/MilepostLayer/arcade/Access Control.arcade | 4 ---- .../arcade/parts/webMercatorToWgs1984.function.arcade | 2 -- 2 files changed, 6 deletions(-) diff --git a/src/layers/MilepostLayer/arcade/Access Control.arcade b/src/layers/MilepostLayer/arcade/Access Control.arcade index 06eebd54..ec562f0a 100644 --- a/src/layers/MilepostLayer/arcade/Access Control.arcade +++ b/src/layers/MilepostLayer/arcade/Access Control.arcade @@ -43,8 +43,6 @@ var shortestDistanceSnapshotDate = null; // Initialize a loop counter var i = 0 for (var f in fs) { - // // Write feature to console. - // Console(f) // Get the distance between the milepost feature // and the current Access Control feature. var d = Distance(f, $feature) @@ -70,8 +68,6 @@ for (var f in fs) { i++; } -Console(`There were ${i} features returned.`) - if (shortestDistanceCode == null) { return null; } else { diff --git a/src/layers/MilepostLayer/arcade/parts/webMercatorToWgs1984.function.arcade b/src/layers/MilepostLayer/arcade/parts/webMercatorToWgs1984.function.arcade index 0a8aa326..63067117 100644 --- a/src/layers/MilepostLayer/arcade/parts/webMercatorToWgs1984.function.arcade +++ b/src/layers/MilepostLayer/arcade/parts/webMercatorToWgs1984.function.arcade @@ -3,8 +3,6 @@ function webMercatorToWgs1984(geom) { var xWebMercator = geom.x; var yWebMercator = geom.y; - Console(xWebMercator); - // Constants var R_MAJOR = 6378137.0; From 90444385ec81a190926f5092dac3964d58807087 Mon Sep 17 00:00:00 2001 From: Jeff Jacobson Date: Thu, 8 Aug 2024 09:02:50 -0700 Subject: [PATCH 4/6] fix: :bug: Eliminated erroneous console error messages --- src/main.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main.ts b/src/main.ts index 2e72feb2..aa812862 100644 --- a/src/main.ts +++ b/src/main.ts @@ -426,10 +426,14 @@ if (!testWebGL2Support()) { }); tempAddResults.addFeatureResults.forEach((r) => { - console.error( - "There was an error adding the temporary graphic where the user clicked.", - r.error, - ); + // r.error CAN be null. Esri's type def. is wrong. + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (r.error != null) { + console.error( + "There was an error adding the temporary graphic where the user clicked.", + r.error, + ); + } }); // Call findNearestRouteLocations From 666707b7c466361e1e9373cd03d74a87ad6d40ec Mon Sep 17 00:00:00 2001 From: Jeff Jacobson Date: Thu, 8 Aug 2024 11:03:17 -0700 Subject: [PATCH 5/6] feat: :sparkles: Added "Copy" action to popups --- src/layers/MilepostLayer/index.ts | 30 +++++++++++++ src/main.ts | 8 ++++ src/setupPopupActions.ts | 72 +++++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 src/setupPopupActions.ts diff --git a/src/layers/MilepostLayer/index.ts b/src/layers/MilepostLayer/index.ts index 9b481971..0a47732a 100644 --- a/src/layers/MilepostLayer/index.ts +++ b/src/layers/MilepostLayer/index.ts @@ -55,6 +55,28 @@ const fields = [ }, ] as FieldProperties[]; +const actionButtonProperties: __esri.ActionButtonProperties[] = [ + { + active: false, + title: "Copy coordinates", + visible: true, + type: "button", + icon: "copy-to-clipboard", + id: "copy", + }, +]; + +async function createActionButtons() { + const [{ default: Collection }, { default: ActionButton }] = + await Promise.all([ + import("@arcgis/core/core/Collection"), + import("@arcgis/core/support/actions/ActionButton"), + ]); + return new Collection>( + actionButtonProperties.map((ap) => new ActionButton(ap)), + ); +} + /** * Creates the {@link FeatureLayer} that displays located mileposts. * @param spatialReference - The {@link SpatialReference} of the layer. @@ -101,6 +123,14 @@ export async function createMilepostLayer(spatialReference: SpatialReference) { visibleFieldNames: new Set(), }); + createActionButtons() + .then((actions) => { + popupTemplate.actions = actions; + }) + .catch((error: unknown) => { + console.error("Error adding action buttons", error); + }); + // Import the Arcade expressions, add them to the popup template, and then // add them to the popup template's fieldInfos array. import("./arcade") diff --git a/src/main.ts b/src/main.ts index aa812862..52b19dcd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -299,6 +299,14 @@ if (!testWebGL2Support()) { popupEnabled: false, }); + import("./setupPopupActions") + .then(({ setupPopupActions }) => { + setupPopupActions(view); + }) + .catch((error: unknown) => { + console.error("Failed to setupPopupActions.", error); + }); + setupSidebarCollapseButton(view); view diff --git a/src/setupPopupActions.ts b/src/setupPopupActions.ts new file mode 100644 index 00000000..263cc311 --- /dev/null +++ b/src/setupPopupActions.ts @@ -0,0 +1,72 @@ +import { on } from "@arcgis/core/core/reactiveUtils"; +import { webMercatorToGeographic } from "@arcgis/core/geometry/support/webMercatorUtils"; + +/** + * Creates a calcite-alert element. + * @returns The created calcite-alert element. + */ +function createCalciteAlert() { + const message = "Coordinates copied to clipboard."; + const alert = document.createElement("calcite-alert"); + alert.kind = "success"; + alert.label = message; + alert.scale = "s"; + alert.autoClose = true; + alert.autoCloseDuration = "fast"; + alert.placement = "top"; + + const messageElement = document.createElement("p"); + messageElement.append(message); + messageElement.slot = "message"; + alert.append(messageElement); + return alert; +} + +/** + * Sets up popup actions for the given map view. + * @param view - The map view to set up popup actions for. + */ +export function setupPopupActions(view: __esri.MapView) { + const alert = createCalciteAlert(); + document.body.append(alert); + const copyPointToClipboard = (point: __esri.Point) => { + const { spatialReference } = point; + if (spatialReference.isWebMercator) { + point = webMercatorToGeographic(point) as __esri.Point; + } else if (!spatialReference.isWGS84) { + throw new Error( + `Unsupported spatial reference: ${spatialReference.wkid}`, + ); + } + + const { x, y } = point; + + navigator.clipboard + .writeText([x, y].join(",")) + .then(() => { + /* __PURE__ */ console.debug("Copied coordinates to clipboard."); + alert.open = true; + }) + .catch((error: unknown) => { + console.error("Failed to copy coordinates.", error); + }); + }; + + function isPoint(g: __esri.Geometry): g is __esri.Point { + return g.type === "point"; + } + + const popupTriggerActionEventHandler: __esri.PopupTriggerActionEventHandler = + (event) => { + /* __PURE__ */ console.debug("trigger-action", event); + if (event.action.id === "copy") { + const feature = view.popup.selectedFeature; + if (isPoint(feature.geometry)) { + copyPointToClipboard(feature.geometry); + } + } + }; + on(() => view.popup, "trigger-action", popupTriggerActionEventHandler); +} + +export default setupPopupActions; From 33c50f5f4e32f9efdb580a37e302bac0205cd9f9 Mon Sep 17 00:00:00 2001 From: Jeff Jacobson Date: Thu, 8 Aug 2024 14:55:03 -0700 Subject: [PATCH 6/6] feat: :sparkles: SR View link only shown when browsing from intranet --- src/common/isIntranet.ts | 13 +++++++++++++ src/layers/MilepostLayer/arcade/index.ts | 16 ++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 src/common/isIntranet.ts diff --git a/src/common/isIntranet.ts b/src/common/isIntranet.ts new file mode 100644 index 00000000..0494611f --- /dev/null +++ b/src/common/isIntranet.ts @@ -0,0 +1,13 @@ +/** + * Tests the current hostname to see if it is on the intranet. + * If the hostname test fails, then tests to see if the browser can + * make a HEAD or GET request (respectively) to an intranet test URL. + * @returns - True if the hostname is on the intranet, false otherwise. + */ +export function isIntranet(): boolean { + return ( + /\.loc$/i.test(location.hostname) || /^localhost$/i.test(location.hostname) + ); +} + +export default isIntranet; diff --git a/src/layers/MilepostLayer/arcade/index.ts b/src/layers/MilepostLayer/arcade/index.ts index d68b5f35..14cd4b33 100644 --- a/src/layers/MilepostLayer/arcade/index.ts +++ b/src/layers/MilepostLayer/arcade/index.ts @@ -1,3 +1,4 @@ +import { isIntranet } from "../../../common/isIntranet"; import AccessControlArcade from "./Access Control.arcade?raw"; import BingMapsArcade from "./Bing Maps.arcade?raw"; import CityArcade from "./City.arcade?raw"; @@ -138,4 +139,19 @@ export const expressions = expressionInfoProperties.map( (info) => new ExpressionInfo(info) as MilepostExpressionInfo, ); +/** + * Removes the SR View URL expression. + */ +const removeSrView = () => { + const x = expressions.find((expression) => expression.name === "srViewURL"); + if (x) { + expressions.splice(expressions.indexOf(x), 1); + } +}; + +// Remove the SR View URL expression if we are not on the intranet. +if (!isIntranet()) { + removeSrView(); +} + export default expressions;