Skip to content

Maps feature updated #1429

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 35 commits into from
Apr 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
a110d06
maps live location, new cursor, updated code in this branch with late…
gup042 Apr 8, 2025
e39b5b6
clearing source before reuse
gup042 Apr 8, 2025
fe2b8a5
add comment for stoptracking, zoom initials
gup042 Apr 8, 2025
e26e970
set center and zoom
gup042 Apr 8, 2025
b2be005
heading and rotation related tweaks, making more precise
gup042 Apr 8, 2025
616ed94
inc radius, rg to try dynamic radius
gup042 Apr 8, 2025
39497a9
direction arrow rot code commit from stash
gup042 Apr 8, 2025
2f2eeac
enhancements to minor specs to improve pef.
gup042 Apr 8, 2025
407d1d9
enhance stop tracking
gup042 Apr 8, 2025
460595e
more tweaks on header alignment agianst the blue dot, header is flaki…
gup042 Apr 8, 2025
9766751
more tweaks on header alignment agianst the blue dot, header is flaki…
gup042 Apr 8, 2025
886f153
css enhancmeents for the scaling on the map
gup042 Apr 9, 2025
0c791ba
css enhancmeents for the scaling on the map
gup042 Apr 9, 2025
ec34614
map cursor live simulation test
gup042 Apr 9, 2025
f8e400f
further improvements in map based on testing, precising location, geo…
gup042 Apr 9, 2025
93af7c1
more..
gup042 Apr 9, 2025
db99ab0
map wrapper advanced css styling to avoid here
gup042 Apr 9, 2025
fc837a3
map wrapper jsx update cleaner code
gup042 Apr 9, 2025
d2d27bb
organised code more orineted
gup042 Apr 9, 2025
a1539f3
handleclose updated and enhanced as per requirements
gup042 Apr 9, 2025
922c0a3
add stash for live gps that was tested before in browser
gup042 Apr 9, 2025
e793ebf
traingle design enhanced
gup042 Apr 9, 2025
9804626
design, accuracy improvements plus functionality that was broken is …
gup042 Apr 9, 2025
5189d96
overall map tested features, updates and cleanuo
gup042 Apr 9, 2025
dbfe48e
viteconfig
gup042 Apr 9, 2025
70c1398
add fake gps simulation in map component
gup042 Apr 15, 2025
ac2e585
lint fixes, imports updates in map wrapper
gup042 Apr 15, 2025
fdb9b8c
accuracy feature, position feature in map based on testing
gup042 Apr 15, 2025
6ab2f29
allowing one point or drop at one time
gup042 Apr 15, 2025
dc56841
maptiler testing, cleaned up traingle feature of the live map
gup042 Apr 15, 2025
bd8503e
updated map wrapper, cleaned and updated with latest map
gup042 Apr 15, 2025
845574d
updated map wrapper, cleaned and updated with lates
gup042 Apr 15, 2025
6e83709
Merge branch 'main' into maps-feature-updated
stevecassidy Apr 17, 2025
949fa06
Convert heading to radians for display
stevecassidy Apr 17, 2025
d6ab86b
centre button uses current location
stevecassidy Apr 21, 2025
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: 3 additions & 3 deletions app/src/gui/components/map/center-control.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {Control} from 'ol/control';
import {Coordinate} from 'ol/coordinate';

Check warning on line 2 in app/src/gui/components/map/center-control.tsx

View workflow job for this annotation

GitHub Actions / Build and Test

'Coordinate' is defined but never used
import {View} from 'ol';
import {CreateDomIcon} from './dom-icon';
import src from '../../../target.svg';
Expand All @@ -8,12 +8,12 @@
* Creates a custom control button that centers the map view to a specified coordinate.
*
* @param {View} view - The map view instance to be controlled.
* @param {Coordinate} center - The coordinate to which the map view should be centered.
* @param {() => void} center - Callback to center the map view.
* @returns {Control} - The custom control instance.
*/
export const createCenterControl = (
view: View,
center: Coordinate
centerMap: () => void
): Control => {
const button = document.createElement('button');
button.className = 'ol-center-button';
Expand All @@ -28,7 +28,7 @@
);

const handleClick = () => {
view.setCenter(center);
centerMap();
};

button.addEventListener('click', handleClick);
Expand Down
233 changes: 191 additions & 42 deletions app/src/gui/components/map/map-component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,15 @@
* See, the License, for the specific language governing permissions and
* limitations under the License.
*
* Description:
* Display a map
/**
* MapComponent.tsx
*
* This component renders an interactive OpenLayers map with support for:
* - Real-time GPS tracking (blue dot, accuracy circle, direction triangle)
* - Offline map support using cached tiles
* - Optional fake GPS simulation for browser testing
* - Dynamic center control and zooming
* - Integration with parent component through callback
*/

import {Box, Grid} from '@mui/material';
Expand All @@ -26,17 +33,30 @@ import VectorLayer from 'ol/layer/Vector';
import Map from 'ol/Map';
import {transform} from 'ol/proj';
import VectorSource from 'ol/source/Vector';
import {Circle, Fill, Stroke, Style} from 'ol/style';
import {useCallback, useEffect, useMemo, useState} from 'react';
import {Fill, RegularShape, Stroke, Style} from 'ol/style';
import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {useIsOnline} from '../../../utils/customHooks';
import {getCoordinates, useCurrentLocation} from '../../../utils/useLocation';
import {createCenterControl} from '../map/center-control';
import {VectorTileStore} from './tile-source';
import Feature from 'ol/Feature';
import {Point} from 'ol/geom';
import CircleStyle from 'ol/style/Circle';
import {Geolocation, Position} from '@capacitor/geolocation';

const defaultMapProjection = 'EPSG:3857';
const MAX_ZOOM = 20;
const MIN_ZOOM = 12;

/**
* Optional fake GPS mode for browser testing. Enable via:
* `window.__USE_FAKE_GPS__ = true` in browser console.
*/ declare global {
interface Window {
__USE_FAKE_GPS__?: boolean;
}
}

/**
* canShowMapNear - can we show a map near this location?
*
Expand Down Expand Up @@ -76,13 +96,20 @@ export interface MapComponentProps {
zoom?: number;
}

/**
* MapComponent
*
* This is the main map rendering component. It:
* - Initializes an OpenLayers map with tile support
* - Renders live GPS location and directional cursor
* - Supports fake GPS simulation via global flag
* - Accepts a parent callback to provide the map instance
*/
export const MapComponent = (props: MapComponentProps) => {
const [map, setMap] = useState<Map | undefined>(undefined);
const [zoomLevel, setZoomLevel] = useState(props.zoom || MIN_ZOOM); // Default zoom level
const [attribution, setAttribution] = useState<string | null>(null);

const {isOnline} = useIsOnline();

const tileStore = useMemo(() => new VectorTileStore(), []);

// Use the custom hook for location
Expand All @@ -97,8 +124,13 @@ export const MapComponent = (props: MapComponentProps) => {
return getCoordinates(currentPosition);
}, [props.center, currentPosition]);

const positionLayerRef = useRef<VectorLayer>();
const watchIdRef = useRef<string | null>(null);
const fakeIntervalRef = useRef<NodeJS.Timeout | null>(null);
const liveLocationRef = useRef<Position | null>(null);

/**
* Create the OpenLayers map element
* Initializes the map instance with base tile layers and zoom controls.
*/
const createMap = useCallback(async (element: HTMLElement): Promise<Map> => {
setAttribution(tileStore.getAttribution() as unknown as string);
Expand Down Expand Up @@ -132,54 +164,165 @@ export const MapComponent = (props: MapComponentProps) => {
}, []);

/**
* Add a marker to the map at the current location
* Initializes and updates the live GPS location cursor.
* Displays:
* - Blue dot at user location with directional traingular and accuracy circle. which would wokk on rela-time gps location tracking
*
* @param theMap the map element
* Also supports fake GPS simulation for browser testing.
*/
const addCurrentLocationMarker = (theMap: Map) => {
const source = new VectorSource();
const geoJson = new GeoJSON();
const startLiveCursor = (theMap: Map) => {
// Clean up before re-adding
if (positionLayerRef.current) {
theMap.removeLayer(positionLayerRef.current);
positionLayerRef.current.getSource()?.clear();
positionLayerRef.current = undefined;
}
if (watchIdRef.current !== null) {
Geolocation.clearWatch({id: watchIdRef.current}).then(() => {
watchIdRef.current = null;
});
}
// to be removed later after testing @TODO: RG
if (fakeIntervalRef.current !== null) {
clearInterval(fakeIntervalRef.current);
fakeIntervalRef.current = null;
}

const stroke = new Stroke({color: '#e2ebef', width: 2});
const layer = new VectorLayer({
source: source,
style: new Style({
image: new Circle({
radius: 10,
fill: new Fill({color: '#465ddf90'}),
stroke: stroke,
}),
}),
});
const view = theMap.getView();
const projection = view.getProjection();
const positionSource = new VectorSource();
const layer = new VectorLayer({source: positionSource, zIndex: 999});
theMap.addLayer(layer);
positionLayerRef.current = layer;

// only do this if we have a real map_center
if (mapCenter) {
const centerFeature = {
type: 'Feature',
geometry: {
type: 'Point',
coordinates: mapCenter,
},
};

// there is only one feature but readFeature return type is odd and readFeatures works for singletons
const theFeatures = geoJson.readFeatures(centerFeature, {
dataProjection: 'EPSG:4326',
featureProjection: theMap.getView().getProjection(),
const dotFeature = new Feature(new Point([0, 0]));
const triangleFeature = new Feature(new Point([0, 0]));
const accuracyFeature = new Feature(new Point([0, 0]));
positionSource.addFeatures([dotFeature, triangleFeature, accuracyFeature]);

const updateCursor = (
coords: number[],
heading: number,
accuracy: number
) => {
const resolution = view.getResolution() ?? 1;

// blue location dot
dotFeature.setGeometry(new Point(coords));
dotFeature.setStyle(
new Style({
image: new CircleStyle({
radius: 14,
fill: new Fill({color: '#1a73e8'}),
stroke: new Stroke({color: '#A19F9FFF', width: 3}),
}),
})
);

// accuracy circle
const accuracyRadius = accuracy / resolution;
accuracyFeature.setGeometry(new Point(coords));
accuracyFeature.setStyle(
new Style({
image: new CircleStyle({
radius: accuracyRadius,
fill: new Fill({color: 'rgba(100, 149, 237, 0.1)'}),
stroke: new Stroke({color: 'rgba(100, 149, 237, 0.4)', width: 1}),
}),
})
);

// heading is in degrees
const headingRadians = (heading * Math.PI) / 180;
// directional navigation triangle
triangleFeature.setGeometry(new Point(coords));
triangleFeature.setStyle(
new Style({
image: new RegularShape({
points: 3,
radius: 12,
rotation: headingRadians + Math.PI,
angle: Math.PI,
fill: new Fill({color: '#1a73e8'}),
stroke: new Stroke({color: 'white', width: 2}),
}),
geometry: () => {
const px = theMap.getPixelFromCoordinate(coords);
const offset = 23;
const dx = offset * Math.sin(headingRadians);
const dy = -offset * Math.cos(headingRadians);
const newPx = [px[0] + dx, px[1] + dy];
return new Point(theMap.getCoordinateFromPixel(newPx));
},
})
);
};

if (!window.__USE_FAKE_GPS__) {
// Add a watch on position, update our live cursor when it changes

Geolocation.watchPosition({enableHighAccuracy: true}, (position, err) => {
if (err) {
console.error(err);
return;
}
if (position) {
liveLocationRef.current = position;
const coords = transform(
[position.coords.longitude, position.coords.latitude],
'EPSG:4326',
projection
);
updateCursor(
coords,
position.coords.heading ?? 0,
position.coords.accuracy ?? 30
);
}
}).then(id => {
watchIdRef.current = id;
});
source.addFeature(theFeatures[0]);
theMap.addLayer(layer);
} else {
/**
* Fake GPS Simulation (for browser testing only)
* To activate, run this in browser console: window.__USE_FAKE_GPS__ = true, might work in browser if no maptiler/map issues
*/
console.log('Fake GPS mode ON');
let angle = 0;
const radius = 0.0001;
const center = transform([151.231, -33.918], 'EPSG:4326', projection);

if (watchIdRef.current) clearInterval(watchIdRef.current);

setInterval(() => {
angle += 0.1;
const coords = [
center[0] + radius * Math.cos(angle),
center[1] + radius * Math.sin(angle),
];
updateCursor(coords, angle, 20);
}, 1000);
}
};

// center the map on the current location
const centerMap = () => {
if (map && liveLocationRef.current) {
const coords = getCoordinates(liveLocationRef.current);
if (coords) {
const center = transform(coords, 'EPSG:4326', defaultMapProjection);
map.getView().setCenter(center);
}
}
};

// when we have a location and a map, add the 'here' marker to the map
useEffect(() => {
if (!loadingLocation && map && mapCenter) {
addCurrentLocationMarker(map);
startLiveCursor(map);
if (mapCenter) {
const center = transform(mapCenter, 'EPSG:4326', defaultMapProjection);
// add the 'here' button to go to the current location
map.addControl(createCenterControl(map.getView(), center));
map.addControl(createCenterControl(map.getView(), centerMap));

// we set the map extent if we were given one or if not,
// set the map center which will either have been passed
Expand Down Expand Up @@ -215,6 +358,12 @@ export const MapComponent = (props: MapComponentProps) => {
[map, createMap]
);

useEffect(() => {
if (map) {
startLiveCursor(map); //reapply live cursor on map init or remount
}
}, [map]);

return (
<>
<Grid container spacing={2}>
Expand Down
29 changes: 29 additions & 0 deletions app/src/gui/fields/maps/MapWrapper.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,32 @@
.mapSubmitButton {
grid-row: 2;
}
/* Custom map style */
.gps-location-dot {
width: 20px;
height: 20px;
background: #1a73e8;
border: 2px solid white;
border-radius: 50%;
box-shadow: 0 0 0 rgba(26, 115, 232, 0.4);
animation: pulse 1.5s infinite;
}

@keyframes pulse {
0% {
transform: scale(0.95);
opacity: 1;
}
70% {
transform: scale(1.3);
opacity: 0;
}
100% {
transform: scale(0.95);
opacity: 0;
}
}
.ol-overlay-container,
.ol-overlay-container-stopevent {
display: none !important;
}
Loading
Loading