From d0ed9c935b2212164f1c8941a80ecd14fb600a51 Mon Sep 17 00:00:00 2001 From: Marc Sallin Date: Fri, 26 Jun 2026 15:10:52 +0200 Subject: [PATCH] @ feat(charts): add "In view" filter to chart list Adds a slide toggle to the chart list that filters the displayed charts to only those whose bounds intersect the current map viewport. The list updates reactively as the user pans/zooms the map. The text filter and the in-view filter can be combined. Charts without bounds metadata are treated as global and remain visible. The viewport-intersection test lives in a pure isChartInView() helper in chart-utils, alongside extentFromBounds, with unit tests covering overlap, containment, edge-touching, disjoint and missing-bounds cases. @ --- .../map/ol/lib/charts/chart-utils.spec.ts | 50 ++++++++++++++++++- .../modules/map/ol/lib/charts/chart-utils.ts | 30 ++++++++++- .../components/charts/chartlist.html | 17 +++++-- .../components/charts/chartlist.ts | 37 ++++++++++++++ 4 files changed, 128 insertions(+), 6 deletions(-) diff --git a/src/app/modules/map/ol/lib/charts/chart-utils.spec.ts b/src/app/modules/map/ol/lib/charts/chart-utils.spec.ts index 9f96ee7b9..7b1faa852 100644 --- a/src/app/modules/map/ol/lib/charts/chart-utils.spec.ts +++ b/src/app/modules/map/ol/lib/charts/chart-utils.spec.ts @@ -1,5 +1,9 @@ import { expect, describe, it } from 'vitest'; -import { extentFromBounds, resolveLayerMaxZoom } from './chart-utils'; +import { + extentFromBounds, + isChartInView, + resolveLayerMaxZoom +} from './chart-utils'; describe('resolveLayerMaxZoom', () => { it('returns chart max when over-zoom disabled', () => { @@ -32,4 +36,48 @@ describe('extentFromBounds', () => { it('returns undefined for invalid bounds values', () => { expect(extentFromBounds([90, 90, 90, 300])).toBe(undefined); }); + + it('returns a transformed extent for valid mid-range bounds', () => { + const extent = extentFromBounds([-10, -10, 10, 10]); + expect(extent).toBeDefined(); + expect(extent).toHaveLength(4); + expect(extent?.every((n) => Number.isFinite(n))).toBe(true); + }); + + it('rejects bounds that touch the +/-180 / +/-90 edges', () => { + expect(extentFromBounds([-180, 0, 10, 10])).toBe(undefined); + expect(extentFromBounds([-10, -90, 10, 10])).toBe(undefined); + expect(extentFromBounds([-10, -10, 180, 10])).toBe(undefined); + expect(extentFromBounds([-10, -10, 10, 90])).toBe(undefined); + }); + + it('accepts bounds just inside the edges', () => { + expect(extentFromBounds([-179.99, -89.99, 179.99, 89.99])).toBeDefined(); + }); +}); + +describe('isChartInView', () => { + const extent = [10, 40, 20, 50]; + + it('keeps a chart whose bounds overlap the extent', () => { + expect(isChartInView([15, 45, 30, 60], extent)).toBe(true); + }); + + it('keeps a chart fully contained within the extent', () => { + expect(isChartInView([12, 42, 18, 48], extent)).toBe(true); + }); + + it('drops a chart whose bounds are disjoint from the extent', () => { + expect(isChartInView([30, 45, 40, 60], extent)).toBe(false); + }); + + it('keeps a chart that only touches the extent edge', () => { + expect(isChartInView([20, 40, 30, 50], extent)).toBe(true); + }); + + it('keeps charts with missing or malformed bounds (treated as global)', () => { + expect(isChartInView(undefined, extent)).toBe(true); + expect(isChartInView([10, 40, 20], extent)).toBe(true); + expect(isChartInView([], extent)).toBe(true); + }); }); diff --git a/src/app/modules/map/ol/lib/charts/chart-utils.ts b/src/app/modules/map/ol/lib/charts/chart-utils.ts index 16eb126b5..f6b3888c5 100644 --- a/src/app/modules/map/ol/lib/charts/chart-utils.ts +++ b/src/app/modules/map/ol/lib/charts/chart-utils.ts @@ -1,4 +1,4 @@ -import { Extent } from 'ol/extent'; +import { Extent, intersects } from 'ol/extent'; import { transformExtent } from 'ol/proj'; export function resolveLayerMaxZoom( @@ -28,3 +28,31 @@ export function extentFromBounds(bounds?: number[]): Extent | undefined { } return transformExtent(bounds, 'EPSG:4326', 'EPSG:3857'); } + +/** + * Determines whether a chart should remain visible when the chart list is + * filtered to the current map view. + * + * Charts without valid bounds metadata are treated as global (not tied to a + * region) and are always kept. Both `bounds` and `extent` are EPSG:4326 + * [minLon, minLat, maxLon, maxLat], so they are compared directly without + * re-projection. + * + * Limitation: this is a plain min/max overlap test and does not account for a + * viewport that crosses the antimeridian (+/-180 longitude); near the dateline a + * chart may be wrongly included or excluded. + * + * Example: + * bounds=[10, 40, 20, 50], extent=[15, 45, 30, 60] -> true (overlap) + * bounds=[10, 40, 20, 50], extent=[30, 45, 40, 60] -> false (disjoint) + * bounds=undefined, extent=[...] -> true (global chart) + */ +export function isChartInView( + bounds: number[] | undefined, + extent: Extent +): boolean { + if (!Array.isArray(bounds) || bounds.length !== 4) { + return true; + } + return intersects(bounds, extent); +} diff --git a/src/app/modules/skresources/components/charts/chartlist.html b/src/app/modules/skresources/components/charts/chartlist.html index e8a9b5e34..3e7b31093 100644 --- a/src/app/modules/skresources/components/charts/chartlist.html +++ b/src/app/modules/skresources/components/charts/chartlist.html @@ -92,20 +92,29 @@ matTooltip="Display Chart Boundaries" matTooltipPosition="above" > - Boundaries + Bounds + + + In view