Skip to content

Commit 946fe74

Browse files
authored
Merge pull request #511 from vsimakhin/feature/customizable-map
customizable map format
2 parents 255545e + 19ee9c9 commit 946fe74

9 files changed

Lines changed: 385 additions & 184 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## [Unreleased]
4+
5+
- New: New map options and map sources (satellite, terrain).
6+
37
## [4.4.0] - 12.05.2026
48

59
- Fix: Remarks were not displayed in the licensing table.

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ The application will automatically create a local SQLite database on the first s
6363

6464
# Changelog
6565

66+
## [Unreleased]
67+
68+
- New: New map options and map sources (satellite, terrain).
69+
6670
## [4.4.0] - 12.05.2026
6771

6872
- Fix: Remarks were not displayed in the licensing table.
@@ -428,6 +432,10 @@ Frontend:
428432
* file-type https://github.com/sindresorhus/file-type
429433
* mapbox/togeojson https://github.com/mapbox/togeojson
430434
435+
Map Sources:
436+
* OpenStreetMap https://www.openstreetmap.org/
437+
* Esri/ArcGIS Online https://www.arcgis.com/
438+
431439
---
432440
433441
# Star History

app/ui/src/assets/map-pin2.png

1.17 KB
Loading

app/ui/src/components/FlightMap/FlightMap.jsx

Lines changed: 20 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,9 @@ import 'ol/ol.css';
44
// openlayers
55
import Map from 'ol/Map';
66
import View from 'ol/View';
7-
import TileLayer from 'ol/layer/Tile';
87
import VectorLayer from 'ol/layer/Vector';
98
import VectorSource from 'ol/source/Vector';
10-
import OSM from 'ol/source/OSM';
119
import FullScreen from 'ol/control/FullScreen';
12-
import Feature from 'ol/Feature';
13-
import Point from 'ol/geom/Point';
14-
import LineString from 'ol/geom/LineString';
15-
import { Style, Icon, Fill, Text } from 'ol/style';
1610
import Overlay from 'ol/Overlay';
1711
import { transform } from 'ol/proj';
1812
// MUI UI elements
@@ -25,14 +19,9 @@ import { queryClient } from '../../util/http/http';
2519
import { fetchAirport } from '../../util/http/airport';
2620
import { DownloadMapButton } from './DownloadMapButton';
2721
import useCustomFields from '../../hooks/useCustomFields';
28-
29-
import icon1 from "../../assets/favicon.ico";
30-
import icon2 from "../../assets/map-pin.png";
31-
32-
const icons = {
33-
ico: { src: icon1, displacement: [0, 0] },
34-
pin: { src: icon2, displacement: [0, 14] }
35-
};
22+
import { DEFAULT_MAP_OPTIONS, drawGreatCircleLine, drawTrackLog, addMarker, MAP_OPTIONS_NAME, getMapBase } from './helpers';
23+
import MapOptionsButton from './MapOptionsButton';
24+
import { CODEC_JSON, useLocalStorageState } from '../../hooks/useLocalStorageState';
3625

3726
const getAirportData = async (id, airportsMap) => {
3827
if (airportsMap) {
@@ -62,95 +51,9 @@ const getAirportData = async (id, airportsMap) => {
6251
}
6352
}
6453

65-
const addMarker = (features, airport, options) => {
66-
/**
67-
* Code string for an airport based on its IATA and ICAO codes.
68-
* If the airport has both IATA and ICAO codes and they are different,
69-
* the code will be in the format "ICAO/IATA". Otherwise, it will just be the ICAO code.
70-
*/
71-
const code = airport.iata && airport.iata !== airport.icao ? `${airport.icao}/${airport.iata}` : airport.icao;
72-
73-
// Check if marker already exists
74-
const exists = features.find(f => f.get('code') === code);
75-
if (exists) return;
76-
77-
const feature = new Feature({
78-
geometry: new Point([airport.lon, airport.lat]).transform('EPSG:4326', 'EPSG:3857'),
79-
code: code,
80-
name: airport.name,
81-
country: airport.country,
82-
city: airport.city,
83-
elevation: airport.elevation,
84-
coordinates: `${airport.lat}, ${airport.lon}`,
85-
});
86-
87-
feature.setStyle(
88-
new Style({
89-
image: new Icon({ ...icons[options.icon] }),
90-
text: options.airport_ids ? new Text({
91-
text: code,
92-
offsetY: -12,
93-
scale: 1.3,
94-
fill: new Fill({
95-
color: '#333',
96-
}),
97-
}) : null,
98-
}),
99-
);
100-
101-
features.push(feature);
102-
}
103-
104-
const createGreatCircleLine = (start, end, segments = 64) => {
105-
const lon1 = start.lon * Math.PI / 180
106-
const lat1 = start.lat * Math.PI / 180
107-
const lon2 = end.lon * Math.PI / 180
108-
const lat2 = end.lat * Math.PI / 180
109-
110-
const coords = []
111-
112-
const d = 2 * Math.asin(Math.sqrt(Math.sin((lat1 - lat2) / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin((lon1 - lon2) / 2) ** 2))
113-
114-
for (let i = 0; i <= segments; i++) {
115-
const f = i / segments
116-
const A = Math.sin((1 - f) * d) / Math.sin(d)
117-
const B = Math.sin(f * d) / Math.sin(d)
118-
const x = A * Math.cos(lat1) * Math.cos(lon1) + B * Math.cos(lat2) * Math.cos(lon2)
119-
const y = A * Math.cos(lat1) * Math.sin(lon1) + B * Math.cos(lat2) * Math.sin(lon2)
120-
const z = A * Math.sin(lat1) + B * Math.sin(lat2)
121-
const lat = Math.atan2(z, Math.sqrt(x * x + y * y))
122-
const lon = Math.atan2(y, x)
123-
124-
coords.push(transform([lon * 180 / Math.PI, lat * 180 / Math.PI], 'EPSG:4326', 'EPSG:3857'))
125-
}
54+
export const FlightMap = ({ data, title = "Flight Map", sx, airportsMap }) => {
55+
const [options] = useLocalStorageState(MAP_OPTIONS_NAME, DEFAULT_MAP_OPTIONS, { codec: CODEC_JSON });
12656

127-
return new LineString(coords)
128-
}
129-
130-
const drawGreatCircleLine = (departure, arrival, vectorSource) => {
131-
const geometry = createGreatCircleLine(departure, arrival)
132-
const routeFeature = new Feature({ geometry, type: 'route' })
133-
vectorSource.addFeature(routeFeature)
134-
}
135-
136-
const drawTrackLog = (flightTrack, vectorSource, flightId) => {
137-
const track = JSON.parse(atob(flightTrack));
138-
139-
const coordinates = track.map((geometry) =>
140-
transform([geometry[1], geometry[0]], 'EPSG:4326', 'EPSG:3857')
141-
);
142-
143-
const lineFeature = new LineString(coordinates);
144-
const feature = new Feature({
145-
geometry: lineFeature,
146-
lineKey: `track-${flightId}`,
147-
});
148-
vectorSource.addFeature(feature);
149-
}
150-
151-
const DEFAULT_OPTIONS = { routes: true, tracks: false, airport_ids: true, icon: 'ico' };
152-
153-
export const FlightMap = ({ data, options = DEFAULT_OPTIONS, title = "Flight Map", sx, airportsMap }) => {
15457
const mapRef = useRef(null);
15558
const containerRef = useRef(null);
15659

@@ -171,9 +74,11 @@ export const FlightMap = ({ data, options = DEFAULT_OPTIONS, title = "Flight Map
17174
overlayRef.current = new Overlay({ element: containerRef.current });
17275
const vectorLayer = new VectorLayer({ source: vectorSourceRef.current });
17376

77+
const mapLayer = getMapBase(options.map_base);
78+
17479
const mapInstance = new Map({
17580
target: mapRef.current,
176-
layers: [new TileLayer({ source: new OSM() }), vectorLayer],
81+
layers: [mapLayer, vectorLayer],
17782
view: new View({
17883
center: transform([10, 45], "EPSG:4326", "EPSG:3857"),
17984
zoom: 4,
@@ -211,7 +116,7 @@ export const FlightMap = ({ data, options = DEFAULT_OPTIONS, title = "Flight Map
211116
mapRefInstance.current = null;
212117
setMap(null);
213118
};
214-
}, []);
119+
}, [options.map_base]);
215120

216121
useEffect(() => {
217122
if (!map || !data) return;
@@ -257,14 +162,14 @@ export const FlightMap = ({ data, options = DEFAULT_OPTIONS, title = "Flight Map
257162

258163
fullRoute.push(arrival);
259164

260-
if (options.routes) {
165+
if (options.routes.enabled) {
261166
for (let i = 0; i < fullRoute.length - 1; i++) {
262-
drawGreatCircleLine(fullRoute[i], fullRoute[i + 1], vectorSourceRef.current);
167+
drawGreatCircleLine(fullRoute[i], fullRoute[i + 1], vectorSourceRef.current, options.routes.color, options.routes.thickness);
263168
}
264169
}
265170

266-
if (options.tracks && flight.track) {
267-
drawTrackLog(flight.track, vectorSourceRef.current, flight.uuid || flight.id);
171+
if (options.tracks.enabled && flight.track) {
172+
drawTrackLog(flight.track, vectorSourceRef.current, flight.uuid || flight.id, options.tracks.color, options.tracks.thickness);
268173
}
269174

270175
totalDistance += flight.distance;
@@ -307,7 +212,13 @@ export const FlightMap = ({ data, options = DEFAULT_OPTIONS, title = "Flight Map
307212
<>
308213
<Card variant="outlined" sx={{ ...sx, height: "85vh", position: "relative" }}>
309214
<CardContent sx={{ height: "100%" }}>
310-
<CardHeader title={title} action={map ? <DownloadMapButton map={map} /> : null} />
215+
<CardHeader title={title}
216+
action={
217+
<>
218+
<DownloadMapButton map={map} />
219+
<MapOptionsButton />
220+
</>
221+
} />
311222
<div ref={mapRef} style={{ width: "100%", height: "calc(100% - 50px)", borderRadius: "4px", overflow: "hidden" }} />
312223
{distance > 0 && (
313224
<Typography sx={{ mt: 1 }}>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { useCallback } from 'react';
2+
// MUI
3+
import IconButton from "@mui/material/IconButton";
4+
import Tooltip from "@mui/material/Tooltip";
5+
// MUI Icons
6+
import SettingsIcon from '@mui/icons-material/Settings';
7+
// Custom
8+
import { useDialogs } from '../../hooks/useDialogs/useDialogs';
9+
import MapOptionsModal from './MapOptionsModal';
10+
11+
export const MapOptionsButton = () => {
12+
const dialogs = useDialogs();
13+
14+
const handleOnClick = useCallback(async () => {
15+
await dialogs.open(MapOptionsModal)
16+
}, [dialogs]);
17+
18+
return (
19+
<Tooltip title="More map options">
20+
<IconButton size="small" onClick={handleOnClick} >
21+
<SettingsIcon />
22+
</IconButton>
23+
</Tooltip>
24+
);
25+
};
26+
27+
export default MapOptionsButton;

0 commit comments

Comments
 (0)