From 67c5b488676e2e370658ec23f47676481aef226c Mon Sep 17 00:00:00 2001 From: Jeremy Myers Date: Tue, 9 Dec 2025 09:59:31 -0500 Subject: [PATCH 1/5] Implement apertures --- src/components/OpenLayersMap.tsx | 5 + src/components/icons/ApertureIcon.tsx | 28 ++++ src/components/layers/AperturesLayer.tsx | 181 +++++++++++++++++++++++ src/components/styles/aperture-layer.css | 6 + 4 files changed, 220 insertions(+) create mode 100644 src/components/icons/ApertureIcon.tsx create mode 100644 src/components/layers/AperturesLayer.tsx create mode 100644 src/components/styles/aperture-layer.css diff --git a/src/components/OpenLayersMap.tsx b/src/components/OpenLayersMap.tsx index 4049df5..10ae71d 100644 --- a/src/components/OpenLayersMap.tsx +++ b/src/components/OpenLayersMap.tsx @@ -56,6 +56,7 @@ import { import { ToggleSwitch } from './ToggleSwitch'; import { CenterMapFeature } from './CenterMapFeature'; import { getHistogramData } from '../utils/fetchUtils'; +import { AperturesLayer } from './layers/AperturesLayer'; export type MapProps = { mapGroups: MapGroupResponse[]; @@ -531,6 +532,10 @@ export function OpenLayersMap({ externalSearchMarkerRef={externalSearchMarkerRef} flipped={flipTiles} /> + + + + + + + ); +} diff --git a/src/components/layers/AperturesLayer.tsx b/src/components/layers/AperturesLayer.tsx new file mode 100644 index 0000000..b98d58d --- /dev/null +++ b/src/components/layers/AperturesLayer.tsx @@ -0,0 +1,181 @@ +import { Map, Feature } from 'ol'; +import { useEffect, useState, useRef } from 'react'; +import { ApertureIcon } from '../icons/ApertureIcon'; +import '../styles/aperture-layer.css'; +import Draw from 'ol/interaction/Draw'; +import VectorSource from 'ol/source/Vector'; +import VectorLayer from 'ol/layer/Vector'; +import { Style, Stroke } from 'ol/style'; +import { Circle } from 'ol/geom'; +import { SERVICE_URL } from '../../configs/mapSettings'; + +type AperturesLayerProps = { + mapRef: React.RefObject; + activeBaselayerId: string | undefined; +}; + +export interface ApertureRequest { + layer_id: string; + ra: number; + dec: number; + radius: number; // in meters +} + +interface ApertureResponse extends ApertureRequest { + mean: number; + std: number; + max: number; + min: number; +} + +export function AperturesLayer({ + mapRef, + activeBaselayerId, +}: AperturesLayerProps) { + const [isDrawing, setIsDrawing] = useState(false); + const [apertures, setApertures] = useState([]); + + const hasMaximum = apertures.length === 3; + const title = hasMaximum + ? 'At maximum number of data overlays' + : 'Add up to 3 data overlays'; + + const sourceRef = useRef(null); + const layerRef = useRef | null>(null); + const apertureOneRef = useRef(null); + const drawRef = useRef(null); + const [newAperture, setNewAperture] = useState( + undefined + ); + + useEffect(() => { + if (!mapRef.current) return; + + const source = new VectorSource(); + const layer = new VectorLayer({ source }); + mapRef.current.addLayer(layer); + + layerRef.current = layer; + + return () => { + mapRef.current?.removeLayer(layer); + }; + }, []); + + useEffect(() => { + const map = mapRef.current; + if (!map || !isDrawing) return; + + // 1. Create vector source + const source = new VectorSource(); + + // 2. Create vector layer + const layer = new VectorLayer({ + source, + }); + layer.setZIndex(1000); + map.addLayer(layer); + + // 3. Create Draw interaction + const draw = new Draw({ + source, + type: 'Circle', + }); + drawRef.current = draw; + map.addInteraction(draw); + + // 4. Handle circle creation + draw.on('drawend', (evt) => { + const feature = evt.feature; + const geom = feature.getGeometry() as Circle; + // Convert geometry to RA/Dec + radius + const [ra, dec] = geom.getCenter(); + const radius = geom.getRadius() * 60; // convert degrees to arcmin + const layer_id = feature.ol_uid; + const circleData: ApertureRequest = { layer_id, ra, dec, radius }; + setNewAperture(circleData); + setIsDrawing(false); + map.removeLayer(layer); + }); + + // 5. Delete circle on click + const handleClick = (evt: any) => { + map.forEachFeatureAtPixel(evt.pixel, (feature) => { + if (source.hasFeature(feature)) { + const id = feature.ol_uid; + console.log(feature); + // source.removeFeature(feature); + // setCircles((prev) => prev.filter((c) => c.id !== id)); + } + }); + }; + map.on('singleclick', handleClick); + + return () => { + // map.un("singleclick", handleClick); + map.removeInteraction(draw); + // map.removeLayer(layer); + }; + }, [isDrawing]); + + useEffect(() => { + if (newAperture === undefined) return; + async function fetchAperture() { + if (newAperture === undefined) return; + const { ra, dec, radius } = newAperture; + const apertureData = await fetch( + `${SERVICE_URL}/analysis/aperture/${activeBaselayerId}?ra=${ra}&dec=${dec}&radius=${radius}` + ); + + const response = (await apertureData.json()) as ApertureResponse; + + setApertures((prev) => prev.concat(response)); + + const source = new VectorSource(); + + const layer = new VectorLayer({ + source, + style: new Style({ + stroke: new Stroke({ width: 2 }), + }), + }); + + mapRef.current?.addLayer(layer); + + const circle = new Circle([ra, dec], radius / 60); + const feature = new Feature(circle); + + source.addFeature(feature); + + if (apertureOneRef.current) { + generateContent(apertureOneRef.current, response); + } + } + + fetchAperture(); + setNewAperture(undefined); + }, [newAperture, activeBaselayerId]); + + return ( + <> +
+ +
+
+ + ); +} + +function generateContent(el: HTMLDivElement, data: ApertureResponse) { + const p = document.createElement('p'); + p.textContent = data.mean.toString(); + el.appendChild(p); +} diff --git a/src/components/styles/aperture-layer.css b/src/components/styles/aperture-layer.css new file mode 100644 index 0000000..2ee24c0 --- /dev/null +++ b/src/components/styles/aperture-layer.css @@ -0,0 +1,6 @@ +.draw-aperture-btn-container { + position: absolute; + top: 128px; + left: 9px; + z-index: 1000; +} From 4cbe2e54fb8f37a97806dd1c60f5bc7c6054587f Mon Sep 17 00:00:00 2001 From: Jeremy Myers Date: Thu, 22 Jan 2026 11:39:41 -0500 Subject: [PATCH 2/5] Refactor and rename layer util function that transforms layer's position --- src/components/BoxMenu.tsx | 4 ++-- src/components/layers/HighlightBoxLayer.tsx | 6 +++--- src/components/layers/SourcesLayer.tsx | 16 ++++++++-------- src/utils/layerUtils.ts | 20 ++++++++++---------- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/components/BoxMenu.tsx b/src/components/BoxMenu.tsx index 9571e26..fddbcc9 100644 --- a/src/components/BoxMenu.tsx +++ b/src/components/BoxMenu.tsx @@ -6,7 +6,7 @@ import { SubmapFileExtensions, } from '../configs/submapConfigs'; import { downloadSubmap } from '../utils/fetchUtils'; -import { transformBoxes } from '../utils/layerUtils'; +import { transformBoxCoords } from '../utils/layerUtils'; import { Map } from 'ol'; type BoxMenuProps = { @@ -42,7 +42,7 @@ export function BoxMenu({ const onDownloadClick = useCallback( (ext: SubmapFileExtensions) => { if (submapData) { - const boxPosition = transformBoxes(boxData, flipped); + const boxPosition = transformBoxCoords(boxData, flipped); downloadSubmap( { ...submapData, diff --git a/src/components/layers/HighlightBoxLayer.tsx b/src/components/layers/HighlightBoxLayer.tsx index e0e4858..44566d2 100644 --- a/src/components/layers/HighlightBoxLayer.tsx +++ b/src/components/layers/HighlightBoxLayer.tsx @@ -7,7 +7,7 @@ import Polygon, { fromExtent } from 'ol/geom/Polygon'; import { MenuIcon } from '../icons/MenuIcon'; import { MapProps } from '../OpenLayersMap'; import { BoxMenu } from '../BoxMenu'; -import { transformBoxes, isBoxSynced } from '../../utils/layerUtils'; +import { transformBoxCoords, isBoxSynced } from '../../utils/layerUtils'; type HightlightBoxLayerProps = { highlightBoxes: MapProps['highlightBoxes']; @@ -106,7 +106,7 @@ export function HighlightBoxLayer({ if (!activeBoxIds.includes(box.id)) return; if (addedLayerIdsRef.current.has(layerId)) return; - const boxPosition = transformBoxes( + const boxPosition = transformBoxCoords( { top_left_ra: box.top_left_ra, top_left_dec: box.top_left_dec, @@ -218,7 +218,7 @@ export function HighlightBoxLayer({ return; } - const newExtent = transformBoxes(currentExtent, flipped); + const newExtent = transformBoxCoords(currentExtent, flipped); box.setCoordinates([ [ [newExtent.top_left_ra, newExtent.top_left_dec], diff --git a/src/components/layers/SourcesLayer.tsx b/src/components/layers/SourcesLayer.tsx index 24d866c..b885c00 100644 --- a/src/components/layers/SourcesLayer.tsx +++ b/src/components/layers/SourcesLayer.tsx @@ -11,7 +11,7 @@ import { createSourcePopupContent, getCatalogMarkerColor, transformCoords, - transformSources, + transformFeatureCoords, } from '../../utils/layerUtils'; import { SourceData } from '../../types/maps'; @@ -54,15 +54,15 @@ export function SourcesLayer({ return; } selectedFeatures.forEach((feature) => { - const { newOverlayCoords, newSourceData } = transformSources( + const { newOverlayCoords, newFeatureData } = transformFeatureCoords( feature, flipped ); popupOverlay.setPosition(newOverlayCoords); if (popupDiv) { - createSourcePopupContent(popupDiv, newSourceData); + createSourcePopupContent(popupDiv, newFeatureData); } - setSelectedSourceId(newSourceData.id); + setSelectedSourceId(newFeatureData.id); }); }, [flipped] @@ -173,17 +173,17 @@ export function SourcesLayer({ if (source instanceof VectorSource) { source.getFeatures().forEach((f: Feature) => { if (f) { - const { newOverlayCoords, newSourceData } = transformSources( + const { newOverlayCoords, newFeatureData } = transformFeatureCoords( f, flipped ); const circle = f.getGeometry() as Circle; circle.setCenter(newOverlayCoords); - if (newSourceData.id === selectedSourceId) { + if (newFeatureData.id === selectedSourceId) { popupOverlayRef.current?.setPosition(newOverlayCoords); - setSelectedSourceId(newSourceData.id); + setSelectedSourceId(newFeatureData.id); if (popupRef.current) { - createSourcePopupContent(popupRef.current, newSourceData); + createSourcePopupContent(popupRef.current, newFeatureData); } } } diff --git a/src/utils/layerUtils.ts b/src/utils/layerUtils.ts index d7134d0..19dbbeb 100644 --- a/src/utils/layerUtils.ts +++ b/src/utils/layerUtils.ts @@ -54,28 +54,28 @@ export function transformCoords( } } -export function transformSources(feature: Feature, flipped: boolean) { - const sourceData = feature.get('sourceData') as SourceData; - let newOverlayCoords = [sourceData.ra, sourceData.dec]; - let newSourceData = { ...sourceData }; +export function transformFeatureCoords(feature: Feature, flipped: boolean) { + const currData = feature.get('data'); + let newOverlayCoords = [currData.ra, currData.dec]; + let newFeatureData = { ...currData }; if (flipped) { newOverlayCoords = transformCoords( - [sourceData.ra, sourceData.dec], + [currData.ra, currData.dec], flipped, 'layer' ); - newSourceData = { - ...sourceData, - ra: sourceData.ra < 0 ? sourceData.ra + 360 : sourceData.ra, + newFeatureData = { + ...currData, + ra: currData.ra < 0 ? currData.ra + 360 : currData.ra, }; } return { newOverlayCoords, - newSourceData, + newFeatureData, }; } -export function transformBoxes(boxExtent: BoxExtent, flipped: boolean) { +export function transformBoxCoords(boxExtent: BoxExtent, flipped: boolean) { const newBoxPosition = { ...boxExtent, }; From 8c113dba98d3980cfe4c039d38570d827212abdc Mon Sep 17 00:00:00 2001 From: Jeremy Myers Date: Thu, 22 Jan 2026 11:40:20 -0500 Subject: [PATCH 3/5] Implement aperture layer --- src/components/OpenLayersMap.tsx | 1 + src/components/layers/AperturesLayer.tsx | 311 ++++++++++++++++------- 2 files changed, 227 insertions(+), 85 deletions(-) diff --git a/src/components/OpenLayersMap.tsx b/src/components/OpenLayersMap.tsx index 80dd4cb..92e30bc 100644 --- a/src/components/OpenLayersMap.tsx +++ b/src/components/OpenLayersMap.tsx @@ -538,6 +538,7 @@ export function OpenLayersMap({ ; activeBaselayerId: string | undefined; + flipped: boolean; }; -export interface ApertureRequest { - layer_id: string; +export interface ApertureRequestData { ra: number; dec: number; radius: number; // in meters } -interface ApertureResponse extends ApertureRequest { +export interface ApertureResponse extends ApertureRequestData { + layer_id: string; mean: number; std: number; max: number; min: number; } +interface ApertureResponseWithId extends ApertureResponse { + id: string; +} + export function AperturesLayer({ mapRef, activeBaselayerId, + flipped, }: AperturesLayerProps) { const [isDrawing, setIsDrawing] = useState(false); - const [apertures, setApertures] = useState([]); + const [apertures, setApertures] = useState([]); + const [selectedApertureId, setSelectedApertureId] = useState< + string | undefined + >(undefined); + const isExternalBaselayer = activeBaselayerId?.includes('external'); const hasMaximum = apertures.length === 3; const title = hasMaximum ? 'At maximum number of data overlays' : 'Add up to 3 data overlays'; - const sourceRef = useRef(null); - const layerRef = useRef | null>(null); - const apertureOneRef = useRef(null); - const drawRef = useRef(null); - const [newAperture, setNewAperture] = useState( - undefined - ); + const aperturesLayerRef = useRef | null>(null); + const aperturesSourceRef = useRef(null); + const selectInteractionRef = useRef