Skip to content

Commit 98a9ef0

Browse files
authored
feat: switch to OpenLayers and ol-stac (#111)
1 parent 51d2803 commit 98a9ef0

File tree

7 files changed

+707
-308
lines changed

7 files changed

+707
-308
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
The format is based on [Keep a Changelog](http://keepachangelog.com/)
44
and this project adheres to [Semantic Versioning](http://semver.org/).
55

6+
## Unreleased
7+
8+
- Switch from leaflet to OpenLayers + ol-stac
9+
610
## 1.0.0
711

812
### User-facing changes

package.json

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,16 @@
1111
"eslint": "^9.21.0",
1212
"eslint-plugin-react": "^7.37.4",
1313
"framer-motion": "^12.4.7",
14-
"leaflet": "^1.9.4",
15-
"leaflet-draw": "^1.0.4",
14+
"ol": "9.2.4",
15+
"ol-layerswitcher": "^4.1.2",
16+
"ol-stac": "^1.0.5",
17+
"proj4": "^2.19.10",
1618
"prop-types": "^15.5.2",
1719
"ramda": "^0.30.1",
1820
"react": "^18.0.0",
1921
"react-datepicker": "^8.1.0",
2022
"react-dom": "^18.0.0",
2123
"react-icons": "^5.5.0",
22-
"react-leaflet": "^4.2.0",
23-
"react-leaflet-draw": "^0.20.6",
2424
"react-markdown": "^10.0.0",
2525
"react-scripts": "5.0.1",
2626
"react-syntax-highlighter": "^15.5.13",
@@ -40,7 +40,6 @@
4040
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
4141
"@testing-library/jest-dom": "^6.6.3",
4242
"@testing-library/react": "^16.2.0",
43-
"@types/leaflet": "^1.9.16",
4443
"@types/react": "^18.0.0",
4544
"@types/react-dom": "^18.0.0",
4645
"@types/react-syntax-highlighter": "^15.5.13",

src/components/CommonTileLayer.tsx

Lines changed: 0 additions & 13 deletions
This file was deleted.

src/components/MapModal.tsx

Lines changed: 164 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useRef } from "react";
1+
import React, { useRef, useEffect } from "react";
22
import {
33
Box,
44
Modal,
@@ -10,12 +10,18 @@ import {
1010
ModalCloseButton,
1111
Button,
1212
} from "@chakra-ui/react";
13-
import { MapContainer, FeatureGroup } from "react-leaflet";
14-
import { EditControl } from "react-leaflet-draw";
15-
import "leaflet/dist/leaflet.css";
16-
import "leaflet-draw/dist/leaflet.draw.css";
17-
import L from "leaflet";
18-
import CommonTileLayer from "./CommonTileLayer";
13+
import Map from "ol/Map";
14+
import View from "ol/View";
15+
import TileLayer from "ol/layer/Tile";
16+
import OSM from "ol/source/OSM";
17+
import VectorLayer from "ol/layer/Vector";
18+
import VectorSource from "ol/source/Vector";
19+
import Draw from "ol/interaction/Draw";
20+
import { fromLonLat, toLonLat } from "ol/proj";
21+
import { Fill, Stroke, Style } from "ol/style";
22+
import { Coordinate } from "ol/coordinate";
23+
import Polygon from "ol/geom/Polygon";
24+
import "ol/ol.css";
1925

2026
interface MapModalProps {
2127
isOpen: boolean;
@@ -24,70 +30,177 @@ interface MapModalProps {
2430
}
2531

2632
const MapModal: React.FC<MapModalProps> = ({ isOpen, onClose, onSubmit }) => {
27-
const featureGroupRef = useRef<L.FeatureGroup>(null);
33+
const mapRef = useRef<HTMLDivElement>(null);
34+
const mapInstanceRef = useRef<Map | null>(null);
35+
const vectorSourceRef = useRef<VectorSource | null>(null);
36+
const drawRef = useRef<Draw | null>(null);
2837

2938
const handleDrawStop = () => {
30-
if (!featureGroupRef.current) return;
39+
if (!vectorSourceRef.current) return;
3140

32-
const layers = featureGroupRef.current.getLayers();
33-
if (!layers.length) return;
41+
const features = vectorSourceRef.current.getFeatures();
42+
if (!features.length) return;
3443

35-
const layer = layers[0] as L.Rectangle;
36-
if (!layer.getBounds) return;
44+
const feature = features[features.length - 1]; // Get the latest drawn feature
45+
const geometry = feature.getGeometry();
46+
if (!geometry) return;
3747

38-
const bounds = layer.getBounds();
48+
const extent = geometry.getExtent();
3949

40-
const southWestLng = bounds.getSouthWest().lng.toFixed(2);
41-
const southWestLat = bounds.getSouthWest().lat.toFixed(2);
42-
const northEastLng = bounds.getNorthEast().lng.toFixed(2);
43-
const northEastLat = bounds.getNorthEast().lat.toFixed(2);
44-
45-
const bbox = `${southWestLng}, ${southWestLat}, ${northEastLng}, ${northEastLat}`;
50+
// Transform from Web Mercator (EPSG:3857) to WGS84 (EPSG:4326)
51+
const bottomLeft = toLonLat([extent[0], extent[1]]);
52+
const topRight = toLonLat([extent[2], extent[3]]);
4653

54+
const bbox = `${bottomLeft[0].toFixed(6)}, ${bottomLeft[1].toFixed(6)}, ${topRight[0].toFixed(6)}, ${topRight[1].toFixed(6)}`;
4755
onSubmit(bbox);
4856
};
4957

5058
const handleClearAll = () => {
51-
if (!featureGroupRef.current) return;
52-
53-
featureGroupRef.current.clearLayers();
59+
if (!vectorSourceRef.current) return;
60+
vectorSourceRef.current.clear();
5461
};
5562

63+
// Initialize map when modal opens
64+
useEffect(() => {
65+
if (!isOpen) return;
66+
67+
// Add a small delay to ensure the modal DOM is fully rendered
68+
const initMap = () => {
69+
if (!mapRef.current) {
70+
console.log("Map ref not available");
71+
return;
72+
}
73+
74+
// Clean up existing map instance
75+
if (mapInstanceRef.current) {
76+
mapInstanceRef.current.dispose();
77+
mapInstanceRef.current = null;
78+
}
79+
80+
try {
81+
console.log("Initializing OpenLayers map...");
82+
83+
const vectorSource = new VectorSource();
84+
vectorSourceRef.current = vectorSource;
85+
86+
const vectorLayer = new VectorLayer({
87+
source: vectorSource,
88+
style: new Style({
89+
fill: new Fill({
90+
color: "rgba(255, 255, 255, 0.2)",
91+
}),
92+
stroke: new Stroke({
93+
color: "#ffcc33",
94+
width: 2,
95+
}),
96+
}),
97+
});
98+
99+
const map = new Map({
100+
target: mapRef.current,
101+
layers: [
102+
new TileLayer({
103+
source: new OSM(),
104+
}),
105+
vectorLayer,
106+
],
107+
view: new View({
108+
center: fromLonLat([0, 0]),
109+
zoom: 0,
110+
}),
111+
});
112+
113+
// Use Box drawing for corner-to-corner rectangle drawing
114+
const draw = new Draw({
115+
source: vectorSource,
116+
type: "Circle",
117+
geometryFunction: (coordinates, geometry) => {
118+
if (
119+
!coordinates ||
120+
!Array.isArray(coordinates) ||
121+
coordinates.length < 2
122+
)
123+
return geometry;
124+
125+
const start = coordinates[0] as Coordinate;
126+
const end = coordinates[1] as Coordinate;
127+
128+
// Create rectangle from two corner points
129+
const minX = Math.min(start[0], end[0]);
130+
const minY = Math.min(start[1], end[1]);
131+
const maxX = Math.max(start[0], end[0]);
132+
const maxY = Math.max(start[1], end[1]);
133+
134+
const coords = [
135+
[
136+
[minX, minY],
137+
[maxX, minY],
138+
[maxX, maxY],
139+
[minX, maxY],
140+
[minX, minY],
141+
],
142+
];
143+
144+
if (!geometry) {
145+
geometry = new Polygon(coords);
146+
} else {
147+
geometry.setCoordinates(coords);
148+
}
149+
150+
return geometry;
151+
},
152+
});
153+
154+
draw.on("drawend", () => {
155+
setTimeout(handleDrawStop, 100);
156+
});
157+
158+
map.addInteraction(draw);
159+
mapInstanceRef.current = map;
160+
drawRef.current = draw;
161+
162+
console.log("Map initialized successfully");
163+
164+
// Force map to update size after initialization
165+
setTimeout(() => {
166+
map.updateSize();
167+
console.log("Map size updated");
168+
}, 100);
169+
} catch (error) {
170+
console.error("Error initializing map:", error);
171+
}
172+
};
173+
174+
// Delay initialization to ensure modal is fully rendered
175+
const timeoutId = setTimeout(initMap, 300);
176+
177+
return () => {
178+
clearTimeout(timeoutId);
179+
if (mapInstanceRef.current) {
180+
mapInstanceRef.current.dispose();
181+
mapInstanceRef.current = null;
182+
}
183+
};
184+
}, [isOpen]);
185+
56186
return (
57187
<Modal isOpen={isOpen} onClose={onClose} size="xl">
58188
<ModalOverlay />
59189
<ModalContent>
60190
<ModalHeader>Draw Bounding Box</ModalHeader>
61191
<ModalCloseButton />
62192
<ModalBody>
63-
<Box height="500px">
64-
<MapContainer
65-
style={{ height: "100%", width: "100%" }}
66-
center={[0, 0]}
67-
zoom={1}
68-
scrollWheelZoom={true}
69-
>
70-
<CommonTileLayer />
71-
<FeatureGroup ref={featureGroupRef}>
72-
<EditControl
73-
position="topright"
74-
onEdited={handleDrawStop}
75-
onCreated={handleDrawStop}
76-
draw={{
77-
rectangle: true,
78-
circle: false,
79-
polyline: false,
80-
polygon: false,
81-
marker: false,
82-
circlemarker: false,
83-
}}
84-
edit={{
85-
edit: false,
86-
remove: true,
87-
}}
88-
/>
89-
</FeatureGroup>
90-
</MapContainer>
193+
<Box height="500px" width="100%">
194+
<div
195+
ref={mapRef}
196+
style={{
197+
height: "500px",
198+
width: "100%",
199+
position: "relative",
200+
backgroundColor: "#f5f5f5",
201+
border: "1px solid #ccc",
202+
}}
203+
/>
91204
</Box>
92205
</ModalBody>
93206
<ModalFooter>

0 commit comments

Comments
 (0)