From 16442bab080dd591d53a0ebdd3f807fa2117c27e Mon Sep 17 00:00:00 2001 From: l5y <220195275+l5yth@users.noreply.github.com> Date: Wed, 12 Nov 2025 20:49:03 +0100 Subject: [PATCH] Tighten map auto-fit behaviour (#435) --- .../__tests__/map-auto-fit-settings.test.js | 47 ++++++++++++++++ web/public/assets/js/app/main.js | 12 ++--- .../assets/js/app/map-auto-fit-settings.js | 54 +++++++++++++++++++ 3 files changed, 107 insertions(+), 6 deletions(-) create mode 100644 web/public/assets/js/app/__tests__/map-auto-fit-settings.test.js create mode 100644 web/public/assets/js/app/map-auto-fit-settings.js diff --git a/web/public/assets/js/app/__tests__/map-auto-fit-settings.test.js b/web/public/assets/js/app/__tests__/map-auto-fit-settings.test.js new file mode 100644 index 0000000..cd86174 --- /dev/null +++ b/web/public/assets/js/app/__tests__/map-auto-fit-settings.test.js @@ -0,0 +1,47 @@ +/* + * Copyright © 2025-26 l5yth & contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { resolveAutoFitBoundsConfig, __testUtils } from '../map-auto-fit-settings.js'; + +const { MINIMUM_AUTO_FIT_RANGE_KM, AUTO_FIT_PADDING_FRACTION } = __testUtils; + +test('resolveAutoFitBoundsConfig returns defaults without a distance limit', () => { + const config = resolveAutoFitBoundsConfig({ hasDistanceLimit: false, maxDistanceKm: null }); + assert.equal(config.paddingFraction, AUTO_FIT_PADDING_FRACTION); + assert.equal(config.minimumRangeKm, MINIMUM_AUTO_FIT_RANGE_KM); +}); + +test('resolveAutoFitBoundsConfig constrains minimum range by the limit radius', () => { + const config = resolveAutoFitBoundsConfig({ hasDistanceLimit: true, maxDistanceKm: 2 }); + assert.equal(config.paddingFraction, AUTO_FIT_PADDING_FRACTION); + assert.ok(config.minimumRangeKm >= MINIMUM_AUTO_FIT_RANGE_KM); + assert.ok(config.minimumRangeKm <= 2); +}); + +test('resolveAutoFitBoundsConfig respects small distance limits', () => { + const config = resolveAutoFitBoundsConfig({ hasDistanceLimit: true, maxDistanceKm: 0.1 }); + assert.equal(config.paddingFraction, AUTO_FIT_PADDING_FRACTION); + assert.equal(config.minimumRangeKm, 0.1); +}); + +test('resolveAutoFitBoundsConfig tolerates invalid input', () => { + const config = resolveAutoFitBoundsConfig({ hasDistanceLimit: true, maxDistanceKm: -5 }); + assert.equal(config.paddingFraction, AUTO_FIT_PADDING_FRACTION); + assert.equal(config.minimumRangeKm, MINIMUM_AUTO_FIT_RANGE_KM); +}); diff --git a/web/public/assets/js/app/main.js b/web/public/assets/js/app/main.js index d9db049..ce682d8 100644 --- a/web/public/assets/js/app/main.js +++ b/web/public/assets/js/app/main.js @@ -16,6 +16,7 @@ import { computeBoundingBox, computeBoundsForPoints, haversineDistanceKm } from './map-bounds.js'; import { createMapAutoFitController } from './map-auto-fit-controller.js'; +import { resolveAutoFitBoundsConfig } from './map-auto-fit-settings.js'; import { attachNodeInfoRefreshToMarker, overlayToPopupNode } from './map-marker-node-info.js'; import { createShortInfoOverlayStack } from './short-info-overlay-manager.js'; import { refreshNodeInformation } from './node-details.js'; @@ -394,6 +395,10 @@ let messagesById = new Map(); ? config.maxDistanceKm : null; const LIMIT_DISTANCE = Number.isFinite(MAX_DISTANCE_KM); + const autoFitBoundsConfig = resolveAutoFitBoundsConfig({ + hasDistanceLimit: LIMIT_DISTANCE, + maxDistanceKm: MAX_DISTANCE_KM + }); const INITIAL_VIEW_PADDING_PX = 12; const AUTO_FIT_PADDING_PX = 12; const MAX_INITIAL_ZOOM = 13; @@ -3587,12 +3592,7 @@ let messagesById = new Map(); }); } if (pts.length && fitBoundsEl && fitBoundsEl.checked) { - const bounds = computeBoundsForPoints(pts, { - paddingFraction: 0.2, - minimumRangeKm: LIMIT_DISTANCE - ? Math.min(Math.max(MAX_DISTANCE_KM * 0.1, 1), MAX_DISTANCE_KM) - : 1 - }); + const bounds = computeBoundsForPoints(pts, { ...autoFitBoundsConfig }); fitMapToBounds(bounds, { animate: false, paddingPx: AUTO_FIT_PADDING_PX }); } overlayStack.cleanupOrphans(); diff --git a/web/public/assets/js/app/map-auto-fit-settings.js b/web/public/assets/js/app/map-auto-fit-settings.js new file mode 100644 index 0000000..1568b11 --- /dev/null +++ b/web/public/assets/js/app/map-auto-fit-settings.js @@ -0,0 +1,54 @@ +/* + * Copyright © 2025-26 l5yth & contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const MINIMUM_AUTO_FIT_RANGE_KM = 0.25; +const AUTO_FIT_PADDING_FRACTION = 0.02; + +/** + * Resolve auto-fit bounds configuration for the active map constraints. + * + * @param {{ hasDistanceLimit: boolean, maxDistanceKm: number | null }} options + * - ``hasDistanceLimit`` indicates whether a maximum display radius is enforced. + * - ``maxDistanceKm`` provides the configured maximum distance in kilometres. + * @returns {{ paddingFraction: number, minimumRangeKm: number }} + * Bounds options suitable for ``computeBoundsForPoints``. + */ +export function resolveAutoFitBoundsConfig({ hasDistanceLimit, maxDistanceKm } = {}) { + const effectiveMaxDistance = Number.isFinite(maxDistanceKm) && maxDistanceKm > 0 + ? maxDistanceKm + : null; + + if (!hasDistanceLimit || !effectiveMaxDistance) { + return { + paddingFraction: AUTO_FIT_PADDING_FRACTION, + minimumRangeKm: MINIMUM_AUTO_FIT_RANGE_KM + }; + } + + const minimumRange = Math.min(MINIMUM_AUTO_FIT_RANGE_KM, effectiveMaxDistance); + const resolvedMinimumRange = Number.isFinite(minimumRange) && minimumRange > 0 + ? minimumRange + : MINIMUM_AUTO_FIT_RANGE_KM; + return { + paddingFraction: AUTO_FIT_PADDING_FRACTION, + minimumRangeKm: resolvedMinimumRange + }; +} + +export const __testUtils = { + MINIMUM_AUTO_FIT_RANGE_KM, + AUTO_FIT_PADDING_FRACTION +};