From 1873b83728b53954ec1612a064afa727bb957733 Mon Sep 17 00:00:00 2001 From: Duck Little Date: Thu, 6 Aug 2026 11:08:52 -0500 Subject: [PATCH 1/3] fix: Defer projection definition for faster load times This was a major hotspot on startup and is now cleaned up! The technique here was to move the `configureProjections` call from being a monolith to only defining the projection (and registering it) at the time it was actually needed. --- src/gm3/application.js | 10 +++++++-- src/gm3/components/coordinate-display.js | 6 ++---- src/gm3/components/measure/calc.js | 5 ++--- src/gm3/jsts.js | 5 ++--- src/gm3/util.js | 27 ++++++++++++++++++++++++ src/index.js | 4 ---- 6 files changed, 41 insertions(+), 16 deletions(-) diff --git a/src/gm3/application.js b/src/gm3/application.js index 0ade24a6..4c00370b 100644 --- a/src/gm3/application.js +++ b/src/gm3/application.js @@ -65,7 +65,13 @@ import { import Mark from "markup-js"; -import { addProjDef, getMapSourceName, getLayerName, FORMAT_OPTIONS, parseQuery } from "./util"; +import { + ensureProjection, + getMapSourceName, + getLayerName, + FORMAT_OPTIONS, + parseQuery, +} from "./util"; import { normalizeFieldValues, normalizeSelection } from "./query/util"; import i18nConfigure from "./i18n"; @@ -905,7 +911,7 @@ class Application { * @param {string} projDef.def - a string definition of the projection, in WKT/Proj format */ addProjection(projDef) { - addProjDef(proj4, projDef.ref, projDef.def); + ensureProjection(projDef.ref, projDef.def); } /* Short hand for toggling the highlight of features. diff --git a/src/gm3/components/coordinate-display.js b/src/gm3/components/coordinate-display.js index a42c2a3c..cfe336fa 100644 --- a/src/gm3/components/coordinate-display.js +++ b/src/gm3/components/coordinate-display.js @@ -26,7 +26,7 @@ import React from "react"; import USNG from "usng-tools-js"; import proj4 from "proj4"; -import { addProjDef } from "../util"; +import { ensureProjection } from "../util"; import * as proj from "ol/proj"; @@ -117,9 +117,7 @@ export default class CoordinateDisplay extends React.Component { if (this.props.projections) { this.projections = []; for (const projection of this.props.projections) { - if (typeof projection.projDef !== "undefined") { - addProjDef(proj4, projection.ref, projection.projDef); - } + ensureProjection(projection.ref, projection.projDef); const isNamedProjection = this.namedProjections.indexOf(projection.ref) !== -1; let isDefinedProjection = false; if (!isNamedProjection) { diff --git a/src/gm3/components/measure/calc.js b/src/gm3/components/measure/calc.js index efa23c97..7114ba6b 100644 --- a/src/gm3/components/measure/calc.js +++ b/src/gm3/components/measure/calc.js @@ -22,8 +22,7 @@ * SOFTWARE. */ -import { getUtmZone } from "../../util"; -import { get as getProjection } from "ol/proj"; +import { ensureProjection, getUtmZone } from "../../util"; import olLineString from "ol/geom/LineString"; export const getBearing = (pointA, pointB, ordinalDictionary) => { @@ -131,7 +130,7 @@ export function getPathSegments(coordinatesLonLat) { export function getSegmentInfo(geom, cursorCoords, isDrawing, ordinalDictionary) { // determine an appropriate utm zone for measurement. - const utmZone = getProjection(getUtmZone(geom.coordinates[0])); + const utmZone = ensureProjection(getUtmZone(geom.coordinates[0])); const coords = [].concat(geom.coordinates); diff --git a/src/gm3/jsts.js b/src/gm3/jsts.js index 7855dac4..7db5903d 100644 --- a/src/gm3/jsts.js +++ b/src/gm3/jsts.js @@ -32,8 +32,7 @@ import { BufferOp, GeoJSONReader, GeoJSONWriter } from "turf-jsts"; import turfUnion from "@turf/union"; -import * as proj from "ol/proj"; -import { jsonToGeom, geomToJson, getUtmZone } from "./util"; +import { ensureProjection, jsonToGeom, geomToJson, getUtmZone } from "./util"; export function buffer(feature, meters) { return bufferFeature(feature, meters).geometry; @@ -59,7 +58,7 @@ export function bufferFeature(feature, meters) { const posPt = getAnchorPoint(feature, [0, 0]); // find the feature's location in UTM space. - const utmZone = proj.get(getUtmZone(posPt)); + const utmZone = ensureProjection(getUtmZone(posPt)); // convert the geometry to an OL geometry let geom = jsonToGeom(feature.geometry); diff --git a/src/gm3/util.js b/src/gm3/util.js index 2b67fc3f..7a1326d7 100644 --- a/src/gm3/util.js +++ b/src/gm3/util.js @@ -23,8 +23,11 @@ */ import Request from "reqwest"; +import proj4 from "proj4"; import GeoJSONFormat from "ol/format/GeoJSON"; +import { get as getProjection } from "ol/proj"; +import { register } from "ol/proj/proj4"; import { featureFilter as createFilter } from "@mapbox/mapbox-gl-style-spec"; @@ -528,6 +531,30 @@ export function addProjDef(p4, code, def) { p4.defs(code, def); } +function getUtmProjectionDef(projCode) { + const match = projCode.match(/^UTM([1-9]|[1-5][0-9]|60)([NS])$/); + if (!match) { + return null; + } + + const zone = parseInt(match[1], 10); + const north = match[2] === "N" ? "north" : "south"; + + return "+proj=utm +zone=" + zone + " +" + north + "+datum=WGS84 +units=m +no_defs"; +} + +export function ensureProjection(projCode, projDef) { + if (!proj4.defs(projCode)) { + const def = projDef || getUtmProjectionDef(projCode); + if (def) { + addProjDef(proj4, projCode, def); + register(proj4); + } + } + + return getProjection(projCode); +} + /* Determine the UTM zone for a point * * @param {Point-like} An array containing [x,y] in WGS84 or NAD83 DD diff --git a/src/index.js b/src/index.js index fb29351b..f9e6bdce 100644 --- a/src/index.js +++ b/src/index.js @@ -49,10 +49,6 @@ import HashTracker from "./gm3/trackers/hash"; import * as util from "./gm3/util"; import * as jsts from "./gm3/jsts"; -import proj4 from "proj4"; - -util.configureProjections(proj4); - var components = { Catalog: Catalog, Map: Map, From e025b06dc7224373aeb03396762302b8fd84741b Mon Sep 17 00:00:00 2001 From: Duck Little Date: Thu, 6 Aug 2026 11:14:55 -0500 Subject: [PATCH 2/3] Add tests, handle fuzzed cases --- src/gm3/components/measure/calc.js | 2 +- src/gm3/util.js | 31 +----------------------------- tests/gm3/util.test.js | 18 +++++++++++++++++ 3 files changed, 20 insertions(+), 31 deletions(-) diff --git a/src/gm3/components/measure/calc.js b/src/gm3/components/measure/calc.js index 7114ba6b..92824a58 100644 --- a/src/gm3/components/measure/calc.js +++ b/src/gm3/components/measure/calc.js @@ -110,7 +110,7 @@ export function getPathSegments(coordinatesLonLat) { } // determine an appropriate utm zone for measurement. - const utmZone = getProjection(getUtmZone(coordinatesLonLat[0])); + const utmZone = ensureProjection(getUtmZone(coordinatesLonLat[0])); const segments = []; for (let i = 1, ii = coordinatesLonLat.length; i < ii; i++) { diff --git a/src/gm3/util.js b/src/gm3/util.js index 7a1326d7..8bb4179f 100644 --- a/src/gm3/util.js +++ b/src/gm3/util.js @@ -490,35 +490,6 @@ export function getFeaturesExtent(mapSource) { return bounds; } -/* Configure a set of projections useful for GeoMoose. - * - * At this point this will just configure the UTM zones - * as they are used to do accurate measurement and buffers. - * - * @param {Proj4} p4 The Proj4 Library. - * - */ -export function configureProjections(p4) { - for (let utmZone = 1; utmZone <= 60; utmZone++) { - for (const north of ["north", "south"]) { - // southern utm zones are 327XX, northern 326XX - const epsgCode = 32600 + utmZone + (north === "north" ? 0 : 100); - - const projId = "EPSG:" + epsgCode; - const projAlias = "UTM" + utmZone + (north === "north" ? "N" : "S"); - // it's nice to have a formulary. - const projString = - "+proj=utm +zone=" + utmZone + " +" + north + "+datum=WGS84 +units=m +no_defs"; - - // set up the standard way of calling the projection - // (using the EPSG Code) - p4.defs(projId, projString); - // add an alias, so it can be referred by 'UTM15N' for example. - p4.defs(projAlias, p4.defs(projId)); - } - } -} - /** * addProjDef * Add a projection definition @@ -531,7 +502,7 @@ export function addProjDef(p4, code, def) { p4.defs(code, def); } -function getUtmProjectionDef(projCode) { +export function getUtmProjectionDef(projCode) { const match = projCode.match(/^UTM([1-9]|[1-5][0-9]|60)([NS])$/); if (!match) { return null; diff --git a/tests/gm3/util.test.js b/tests/gm3/util.test.js index d5f0dcba..dda14333 100644 --- a/tests/gm3/util.test.js +++ b/tests/gm3/util.test.js @@ -101,6 +101,24 @@ test("getUtmZone", () => { expect(util.getUtmZone([-93, 45])).toBe("UTM15N"); }); +test("getUtmProjectionDef", () => { + expect(util.getUtmProjectionDef("UTM15N")).toBe( + "+proj=utm +zone=15 +north+datum=WGS84 +units=m +no_defs" + ); + expect(util.getUtmProjectionDef("UTM21S")).toBe( + "+proj=utm +zone=21 +south+datum=WGS84 +units=m +no_defs" + ); +}); + +test("getUtmProjectionDef anti-cases", () => { + expect(util.getUtmProjectionDef("UTM0N")).toBe(null); + expect(util.getUtmProjectionDef("UTM61N")).toBe(null); + expect(util.getUtmProjectionDef("UTM15")).toBe(null); + expect(util.getUtmProjectionDef("UTM15n")).toBe(null); + expect(util.getUtmProjectionDef("EPSG:32615")).toBe(null); + expect(util.getUtmProjectionDef("foo")).toBe(null); +}); + test("metersLengthToUnits", () => { expect(util.metersLengthToUnits(1, "ft")).toBe(1 / 0.3048); expect(util.metersLengthToUnits(1, "mi")).toBe(1 / 1609.347); From 552066e46e3c52de11108eed368b8507c199ac86 Mon Sep 17 00:00:00 2001 From: Duck Little Date: Sat, 8 Aug 2026 16:42:57 -0500 Subject: [PATCH 3/3] drop use of configureProjections in tests --- tests/gm3/components/coordinates.test.js | 8 +------- tests/gm3/components/measure.test.js | 19 ------------------- tests/gm3/jsts.test.js | 10 ---------- 3 files changed, 1 insertion(+), 36 deletions(-) diff --git a/tests/gm3/components/coordinates.test.js b/tests/gm3/components/coordinates.test.js index 0d8d7ec5..ce1ebd83 100644 --- a/tests/gm3/components/coordinates.test.js +++ b/tests/gm3/components/coordinates.test.js @@ -26,11 +26,6 @@ import React from "react"; import { render } from "@testing-library/react"; import CoordinateDisplay, { formatCoordinates } from "gm3/components/coordinate-display"; -// this is necessary to configure the UTM projections -import { configureProjections } from "gm3/util"; -import { register } from "ol/proj/proj4"; -import proj4 from "proj4"; - describe("coordinate display component", () => { it("formats coordinates with default precision", () => { const proj = { @@ -63,12 +58,11 @@ describe("coordinate display component", () => { }); it("accepts a custom projections list", () => { - configureProjections(proj4); - register(proj4); const projections = [ { label: "UTM", ref: "EPSG:32615", + projDef: "+proj=utm +zone=15 +north +datum=WGS84 +units=m +no_defs", precision: 0, }, { diff --git a/tests/gm3/components/measure.test.js b/tests/gm3/components/measure.test.js index 037a985b..f8689bd4 100644 --- a/tests/gm3/components/measure.test.js +++ b/tests/gm3/components/measure.test.js @@ -30,10 +30,6 @@ import { getMeasureLabelStyles, } from "gm3/components/measure/labels"; -// necessary to configure the UTM projections used by getPathSegments -import { configureProjections } from "gm3/util"; -import { register } from "ol/proj/proj4"; -import proj4 from "proj4"; import LineString from "ol/geom/LineString"; import Polygon from "ol/geom/Polygon"; import { fromLonLat } from "ol/proj"; @@ -83,11 +79,6 @@ describe("getBearing tests", () => { */ describe("getPathSegments tests", () => { - beforeEach(() => { - configureProjections(proj4); - register(proj4); - }); - test("returns nothing for paths with fewer than two points", () => { expect(getPathSegments([])).toEqual([]); expect(getPathSegments([[0, 45]])).toEqual([]); @@ -119,11 +110,6 @@ describe("getPathSegments tests", () => { */ describe("getSegmentLabelStyles tests", () => { - beforeEach(() => { - configureProjections(proj4); - register(proj4); - }); - test("builds one label per line segment", () => { const geometry = new LineString([ fromLonLat([-93.0, 45.0]), @@ -182,11 +168,6 @@ describe("getComplementaryUnit tests", () => { }); describe("getAreaLabelStyles tests", () => { - beforeEach(() => { - configureProjections(proj4); - register(proj4); - }); - const polygon = new Polygon([ [ [-93.0, 45.0], diff --git a/tests/gm3/jsts.test.js b/tests/gm3/jsts.test.js index 470ca488..54ba6586 100644 --- a/tests/gm3/jsts.test.js +++ b/tests/gm3/jsts.test.js @@ -24,17 +24,7 @@ import * as jsts from "gm3/jsts"; -// this is necessary to configure the UTM projections -import { configureProjections } from "gm3/util"; -import { register } from "ol/proj/proj4"; -import proj4 from "proj4"; - describe("test basic jsts stuff", function () { - beforeEach(() => { - configureProjections(proj4); - register(proj4); - }); - it("buffers a point", function () { const point = { type: "Feature",