Skip to content

Commit

Permalink
fix: cannot copy coordinates from non-point features (#497)
Browse files Browse the repository at this point in the history
* chore: 🧑‍💻 Configure syntax highlighting for Arcade files

* build: ⬆️ Upgrade dependencies

* refactor: modified line segment label scale

* build: ⬆️ upgrade pnpm

* build: ⬆️ upgrade dependencies

* refactor: Add "Last Coordinate" Arcade script

* fix: 🐛 copy popup action not working with non-point geometry
  • Loading branch information
JeffJacobson authored Jan 15, 2025
1 parent 2d12fce commit 28c3d39
Show file tree
Hide file tree
Showing 8 changed files with 505 additions and 287 deletions.
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,8 @@
},
"[html]": {
"editor.defaultFormatter": "vscode.html-language-features"
},
"files.associations": {
"*.{lxp,arcade}": "javascript"
}
}
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,18 @@
"@biomejs/biome": "1.9.4",
"@types/arcgis-rest-api": "^10.4.8",
"@types/browser-update": "^3.3.3",
"@types/node": "^22.10.5",
"@types/node": "^22.10.6",
"@vitest/coverage-istanbul": "^2.1.8",
"@vitest/coverage-v8": "^2.1.8",
"browserslist-to-esbuild": "^2.1.1",
"cspell": "^8.17.1",
"jsdom": "^25.0.1",
"cspell": "^8.17.2",
"jsdom": "^26.0.0",
"msw": "^2.7.0",
"optionator": "^0.9.4",
"svgo": "^3.3.2",
"svgson": "^5.3.1",
"tsx": "^4.19.2",
"typescript": "^5.7.2",
"typescript": "^5.7.3",
"vite": "^6.0.7",
"vitest": "^2.1.8"
},
Expand All @@ -68,5 +68,5 @@
],
"author": "Jeff Jacobson",
"license": "Unlicense",
"packageManager": "pnpm@9.15.3+sha512.1f79bc245a66eb0b07c5d4d83131240774642caaa86ef7d0434ab47c0d16f66b04e21e0c086eb61e62c77efc4d7f7ec071afad3796af64892fae66509173893a"
"packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0"
}
586 changes: 338 additions & 248 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

65 changes: 65 additions & 0 deletions src/layers/MilepostLayer/arcade/Last Coordinate.arcade
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* @fileoverview
* This Arcade script will return the last point in the input geometry,
* converted to 3857 (Web Mercator Auxiliary Sphere) spatial reference,
* as a comma-separated string of [lat, lon] coordinates.
*/

/**
* Returns the last point in the input geometry, or in the case of a
* Point, that same point will be returned.
* @returns {Point|null} - If input was point, that same point will be returned.
* If input was polyline or polygon, the last point will be returned.
*/
function getPoint() {
var g = Geometry($feature);
var gType = TypeOf(g);
if (gType == "Point") {
return g;
}

/**
* The output point. If a point can't be found, this value
* will remain at its initial null value.
* @type {Point|null}
*/
var p = null;

if (gType == "Polyline") {
var paths = Array(g.paths);
var path = Pop(paths);
p = Pop(Array(path));
}

// Untested
if (gType == "Polygon") {
var rings = Array(g.rings)
var ring = Pop(rings)
var line = Pop(Array(ring))
p = Pop(Array(line))
}

Console(`Error: Unable to find last point in input geometry: ${g}`)

return p;
}

/**
* Create a function to convert meters to lat, long
* @param {Point} g - A point with 3857 (Web Mercator Auxiliary Sphere) spatial reference
* @returns {[number, number]} - an array with two number elements, [y,x].
*/
function MetersToLatLon(g) {
if (IsEmpty(g)) {
return null;
}
var originShift = 2.0 * PI * 6378137.0 / 2.0;
var lon = g.x / originShift * 180.0;
var lat = g.y / originShift * 180.0;
lat = 180.0 / PI * (2.0 * Atan(Exp(lat * PI / 180.0)) - PI / 2.0);
return [lat, lon];
}

var p = MetersToLatLon(getPoint());

return Concatenate(p, ",");
6 changes: 3 additions & 3 deletions src/layers/MilepostLayer/arcade/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,15 @@ import { isInternal } from "../../../urls/isIntranet";
import AccessControlArcade from "./Access Control.arcade?raw";
import CityArcade from "./City.arcade?raw";
import CountyArcade from "./County.arcade?raw";
import lastCoordinateArcade from "./Last Coordinate.arcade?raw";
import LocateMPUrlArcade from "./LocateMP URL.arcade?raw";
import LocationLinksArcade from "./Location Links.arcade?raw";
import MilepostLabelArcade from "./Milepost Label.arcade?raw";
import RegionArcade from "./Region.arcade?raw";
import routeSegmentLabelArcade from "./Route Segment Label.arcade?raw";
import SRViewURLArcade from "./SRView URL.arcade?raw";
import TownshipSectionArcade from "./Township Section.arcade?raw";
import Wgs1984CoordinatesArcade from "./WGS 1984 Coordinates.arcade?raw";
import splitRouteIdFunction from "./parts/splitRouteId.function.arcade?raw";
import webMercatorToWgs1984 from "./parts/webMercatorToWgs1984.function.arcade?raw";

export const locationLinksContent = new ExpressionContent({
expressionInfo: {
Expand Down Expand Up @@ -88,7 +87,8 @@ const expressionInfoProperties = [
{
name: "webMercatorToWgs1984",
title: "GPS Coordinates",
expression: [webMercatorToWgs1984, Wgs1984CoordinatesArcade].join("\n"),
expression: lastCoordinateArcade,
returnType: "string",
},
{
name: "locateMPUrl",
Expand Down
2 changes: 1 addition & 1 deletion src/layers/MilepostLayer/milepost-line-layer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const lineSegmentLabelClass = new LabelClass({
labelExpressionInfo: routeSegmentLabelExpressionInfo,
labelPlacement: "above-along",
labelPosition: "curved",
minScale: 2256.9943525,
minScale: 144447.638572,
allowOverrun: true,
repeatLabel: false,
symbol: new TextSymbol({
Expand Down
4 changes: 4 additions & 0 deletions src/layers/MilepostLayer/milepost-point-layer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ export function createMilepostPointLayer(spatialReference: SpatialReference) {
return milepostLayer;
}

/**
* Creates the {@link SimpleRenderer} for the milepost layer.
* @returns - The renderer
*/
function createRenderer() {
const renderer = new SimpleRenderer({
symbol: milepostSymbol,
Expand Down
116 changes: 86 additions & 30 deletions src/setupPopupActions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { on } from "@arcgis/core/core/reactiveUtils";
import type Geometry from "@arcgis/core/geometry/Geometry";
import Point from "@arcgis/core/geometry/Point";
import { webMercatorToGeographic } from "@arcgis/core/geometry/support/webMercatorUtils";

/**
Expand All @@ -22,6 +24,76 @@ function createCalciteAlert() {
return { alert, messageElement };
}

const copyPointToClipboard = (
point: __esri.Point,
{ messageElement, alert }: ReturnType<typeof createCalciteAlert>,
) => {
let projectedPoint = point;
const { spatialReference } = point;
if (spatialReference.isWebMercator) {
projectedPoint = webMercatorToGeographic(point) as __esri.Point;
} else if (!spatialReference.isWGS84) {
throw new Error(`Unsupported spatial reference: ${spatialReference.wkid}`);
}

const { x, y } = projectedPoint;
messageElement.textContent = `Copied ${[y, x].map((x) => x.toFixed(3)).join(",")} to clipboard.`;

navigator.clipboard
.writeText([y, x].join(","))
.then(() => {
alert.open = true;
})
.catch((error: unknown) => {
console.error("Failed to copy coordinates.", error);
});
};

const isPoint = (g: __esri.Geometry): g is __esri.Point => {
return g.type === "point";
};
const isPolyline = (g: __esri.Geometry): g is __esri.Polyline => {
return g.type === "polyline";
};

const isPolygon = (g: __esri.Geometry): g is __esri.Polygon => {
return g.type === "polygon";
};

const isMultipoint = (g: __esri.Geometry): g is __esri.Multipoint => {
return g.type === "multipoint";
};

/**
* Returns the last point in the input geometry.
* @param g A geometry from the ArcGIS API for JavaScript.
* @returns The last point in the input geometry.
* @throws {TypeError} If the input geometry is not a point, polyline, or polygon.
*/
const getLastPoint = (g: Geometry): Point => {
if (isPoint(g)) {
return g;
}
if (isMultipoint(g)) {
const [x, y] = g.points[g.points.length - 1];
return new Point({ x, y, spatialReference: g.spatialReference });
}
const pathOrRing = isPolyline(g) ? g.paths : isPolygon(g) ? g.rings : null;
if (!pathOrRing) {
throw new TypeError(
"Expected geometry to be a point, polyline, or polygon.",
{
cause: g,
},
);
}
const [x, y] =
pathOrRing[pathOrRing.length - 1][
pathOrRing[pathOrRing.length - 1].length - 1
];
return new Point({ x, y, spatialReference: g.spatialReference });
};

/**
* Sets up popup actions for the given map view.
* @param view - The map view to set up popup actions for.
Expand All @@ -30,40 +102,24 @@ export function setupPopupActions(view: __esri.MapView) {
const { alert, messageElement } = createCalciteAlert();
document.body.append(alert);

const copyPointToClipboard = (point: __esri.Point) => {
let projectedPoint = point;
const { spatialReference } = point;
if (spatialReference.isWebMercator) {
projectedPoint = webMercatorToGeographic(point) as __esri.Point;
} else if (!spatialReference.isWGS84) {
throw new Error(
`Unsupported spatial reference: ${spatialReference.wkid}`,
);
}

const { x, y } = projectedPoint;
messageElement.textContent = `Copied ${[y, x].map((x) => x.toFixed(3)).join(",")} to clipboard.`;

navigator.clipboard
.writeText([y, x].join(","))
.then(() => {
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) => {
if (event.action.id === "copy") {
const feature = view.popup.selectedFeature;
if (isPoint(feature.geometry)) {
copyPointToClipboard(feature.geometry);
try {
const lastPoint = getLastPoint(feature.geometry);
if (lastPoint) {
copyPointToClipboard(lastPoint, { alert, messageElement });
}
} catch (error) {
if (error instanceof Error) {
console.error(error.message, error);
}
if (error instanceof TypeError) {
messageElement.textContent = "Failed to copy coordinates.";
} else {
throw error;
}
}
}
};
Expand Down

0 comments on commit 28c3d39

Please sign in to comment.