Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/app-bundles/download-bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,10 @@ const downloadBundle = createRestBundle({
}
return downloads.filter(
(d) =>
d.status === 'INITIATED' &&
d.progress < 100 &&
// Continue polling if status is INITIATED OR progress < 100 (not both)
// This ensures we keep polling until we receive a response where BOTH
// status != 'INITIATED' AND progress == 100
(d.status === 'INITIATED' || d.progress < 100) &&
// it is possible a stuck download ('INITIATED' and < 100 progress) could remain in the database.
// When that happens this function will call the actionCreator doDownloadPoll
// in an infinite loop forever. Only evaluate downloads from the past 8 hours.
Expand Down
44 changes: 42 additions & 2 deletions src/app-bundles/user-regions-bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ export default createRestBundle({
// Override the save method to return response status
doUserRegionSave: (item) => ({ dispatch, store }) => {
const authToken = store.selectAuthToken();

dispatch({ type: 'USER_REGION_SAVE_START' });

return fetch(`${apiURL}/user-regions`, {
method: 'POST',
headers: {
Expand Down Expand Up @@ -60,6 +60,46 @@ export default createRestBundle({
return { success: false, error };
});
},

// Update existing region using PUT
doUserRegionUpdate: (item) => ({ dispatch, store }) => {
const authToken = store.selectAuthToken();

dispatch({ type: 'USER_REGION_UPDATE_START' });

return fetch(`${apiURL}/user-regions/${item.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + authToken,
},
body: JSON.stringify(item),
})
.then(async response => {
if (response.ok) {
const data = await response.json();
dispatch({
type: 'USER_REGION_SAVE_FINISHED',
payload: data
});
return { success: true, data };
} else {
const errorText = await response.text();
dispatch({
type: 'USER_REGION_UPDATE_ERROR',
payload: { status: response.status, message: errorText }
});
return { success: false, status: response.status, message: errorText };
}
})
.catch(error => {
dispatch({
type: 'USER_REGION_UPDATE_ERROR',
payload: error
});
return { success: false, error };
});
},

// Select public regions
selectUserRegionPublicItems: (state) => {
Expand Down
76 changes: 76 additions & 0 deletions src/app-components/cumulus-map/cumulus-map-base.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { useEffect } from 'react';
import { MapContainer, Marker, Popup, TileLayer } from 'react-leaflet';
import { useMap } from 'react-leaflet/hooks';

// Component to handle map invalidation on resize
function MapInvalidator({ mapHeight }) {
const map = useMap();

useEffect(() => {
if (map && mapHeight) {
// Small delay to let DOM update
const timer = setTimeout(() => {
map.invalidateSize();
}, 100);

return () => clearTimeout(timer);
}
}, [map, mapHeight]);

return null;
}

/**
* Base Cumulus Map Container
*
* A reusable Leaflet map component that provides the core map functionality
* for all map modes in the application.
*
* @param {Object} props
* @param {React.ReactNode} props.children - Mode-specific overlays and controls
* @param {number} [props.mapHeight] - Height of the map for resizing
* @param {Array} [props.center=[37.1, -95.7]] - Map center coordinates [lat, lng]
* @param {number} [props.zoom=4] - Initial zoom level
* @param {Array} [props.locations=[]] - Optional markers to display
*/
export default function CumulusMapBase({
children,
mapHeight,
center = [37.1, -95.7],
zoom = 4,
locations = []
}) {
return (
<div className='map-container h-full'>
<MapContainer
center={center}
zoom={zoom}
scrollWheelZoom={false}
>
<TileLayer
url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
/>

{/* Handle map invalidation when height changes */}
<MapInvalidator mapHeight={mapHeight} />

{/* Mode-specific overlays and controls */}
{children}

{/* Optional location markers */}
{locations.map((loc, i) => (
<Marker
key={`loc-${i}`}
position={[loc.location.lat, loc.location.lon]}
>
<Popup>
<h2>{loc.name}</h2>
<p>{loc.description}</p>
</Popup>
</Marker>
))}
</MapContainer>
</div>
);
}
114 changes: 114 additions & 0 deletions src/app-components/cumulus-map/cumulus-map.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { useRef, forwardRef } from 'react';
import CumulusMapBase from './cumulus-map-base';
import RegionDefineOverlay from './modes/region-define/region-define-overlay';
import UsaLabelOverlay from './modes/usa-label/usa-label-overlay';
import RegionDefineControls from './modes/region-define/region-define-controls';
import UsaLabelControls from './modes/usa-label/usa-label-controls';

/**
* Stable wrapper component for rendering mode-specific overlays
* This component type never changes, only the internal component does
*/
const OverlayRenderer = forwardRef(({ mode, modeConfig, ...props }, ref) => {
const config = modeConfig[mode] || modeConfig['region-define'];
const Component = config.overlay;
return <Component ref={ref} {...props} />;
});
OverlayRenderer.displayName = 'OverlayRenderer';

/**
* Stable wrapper component for rendering mode-specific controls
* This component type never changes, only the internal component does
*/
const ControlsRenderer = ({ mode, modeConfig, children, ...props }) => {
const config = modeConfig[mode] || modeConfig['region-define'];
const Component = config.controls;
return <Component {...props}>{children}</Component>;
};

/**
* Cumulus Map - Main Map Controller
*
* A flexible map component that supports multiple operational modes.
* Modes can be switched by passing the `mode` prop.
*
* The base map persists across mode changes - only overlays and controls swap out.
* This prevents the map from reloading tiles when switching modes.
*
* Available modes:
* - 'region-define': Interactive region drawing and management
* - 'usa-label': Simple USA label display
*
* To add a new mode:
* 1. Create overlay component in modes/<mode-name>/<mode-name>-overlay.js
* 2. Create controls component in modes/<mode-name>/<mode-name>-controls.js
* 3. Import them and add to modeConfig below
*
* @param {Object} props
* @param {string} [props.mode='region-define'] - The map mode to display
* @param {number} [props.mapHeight] - Height of the map
*
* @example
* // Region define mode
* <CumulusMap mode="region-define" mapHeight={500} />
*
* @example
* // USA label mode
* <CumulusMap mode="usa-label" mapHeight={500} />
*/
export default function CumulusMap({
mode = 'region-define',
mapHeight,
...props
}) {
const mapOverlayRef = useRef(null);

// Map mode configurations - add new modes here
const modeConfig = {
'region-define': {
overlay: RegionDefineOverlay,
controls: RegionDefineControls,
},
'usa-label': {
overlay: UsaLabelOverlay,
controls: UsaLabelControls,
},
};

return (
<div className='h-full bg-slate-100 flex flex-col relative'>
<div className='flex-1 relative overflow-hidden'>
{/* Persistent base map */}
<CumulusMapBase mapHeight={mapHeight}>
{/* Mode-specific overlay via stable wrapper */}
<OverlayRenderer
mode={mode}
modeConfig={modeConfig}
ref={mapOverlayRef}
{...props}
/>
</CumulusMapBase>

{/* Mode-specific map overlay controls via stable wrapper */}
<ControlsRenderer
mode={mode}
modeConfig={modeConfig}
mapOverlayRef={mapOverlayRef}
{...props}
>
{({ mapOverlay }) => mapOverlay}
</ControlsRenderer>
</div>

{/* Render footer controls below the map via stable wrapper */}
<ControlsRenderer
mode={mode}
modeConfig={modeConfig}
mapOverlayRef={mapOverlayRef}
{...props}
>
{({ footer }) => footer}
</ControlsRenderer>
</div>
);
}
16 changes: 16 additions & 0 deletions src/app-components/cumulus-map/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Cumulus Map - Main Export
*
* This index file provides a clean way to import the map component:
* import CumulusMap from 'app-components/cumulus-map';
*/
export { default } from './cumulus-map';
export { default as CumulusMapBase } from './cumulus-map-base';

// Region Define Mode exports
export { default as RegionDefineOverlay } from './modes/region-define/region-define-overlay';
export { default as RegionDefineControls } from './modes/region-define/region-define-controls';

// USA Label Mode exports
export { default as UsaLabelOverlay } from './modes/usa-label/usa-label-overlay';
export { default as UsaLabelControls } from './modes/usa-label/usa-label-controls';
Loading