Skip to content

Commit

Permalink
Merge pull request #5 from korsun/mapService
Browse files Browse the repository at this point in the history
Merge pull request #4
  • Loading branch information
korsun authored Mar 29, 2024
2 parents 95945a3 + 5196669 commit 319c6e7
Show file tree
Hide file tree
Showing 11 changed files with 259 additions and 185 deletions.
2 changes: 1 addition & 1 deletion client/src/components/Map/Map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useMap } from '@/hooks';
import s from './Map.module.css';

export const Map = () => {
const { mapRef } = useMap();
const { mapRef } = useMap({ styles: s });

return <div ref={mapRef} className={s.mapContainer} />;
};
16 changes: 16 additions & 0 deletions client/src/helpers/EventEmitter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { createNanoEvents } from 'nanoevents';

export class EventEmitter {
private emitter: ReturnType<typeof createNanoEvents>;

constructor() {
this.emitter = createNanoEvents();
}

public emit = (name: string, data?: unknown, duration?: number) => {
this.emitter.emit(name, data, duration);
};

public onEvent = (eventName: string, fn: AnyFunction) =>
this.emitter.on(eventName, fn);
}
5 changes: 5 additions & 0 deletions client/src/helpers/constants.ts
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
export const MAX_POINTS_WITH_FREE_API = 5;
export const ROUTE_LINE_STYLES = {
'fill-color': 'rgba(255, 255, 255, 0.2)',
'stroke-color': 'red',
'stroke-width': 5,
};
1 change: 1 addition & 0 deletions client/src/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export { getCurrentPosition, getRetinaMod };
export * from './constants';
export * from './route';
export * from './mapMarkerStyles';
export { EventEmitter } from './EventEmitter';
231 changes: 48 additions & 183 deletions client/src/hooks/useMap.tsx
Original file line number Diff line number Diff line change
@@ -1,199 +1,64 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import Map from 'ol/Map';
// import OSM from 'ol/source/OSM.js'
import { Tile as TileLayer, Vector as VectorLayer } from 'ol/layer';
import { Vector as VectorSource, XYZ } from 'ol/source';
import View from 'ol/View';
import { useGeographic } from 'ol/proj';
import {
Draw,
Modify,
Snap,
defaults as defaultInteractions,
} from 'ol/interaction';
import GeoJSON from 'ol/format/GeoJSON';
import { Attribution, ScaleLine, Zoom } from 'ol/control';
import { Point } from 'ol/geom';
import { useEffect, useMemo, useRef, useState } from 'react';
import { Coordinate } from 'ol/coordinate';

import {
endMarkerStyle,
getCurrentPosition,
getRetinaMod,
interimMarkerStyle,
sortMarkersById,
startMarkerStyle,
} from '@/helpers';
import { MapService } from '@/services';
import { fetchRoute } from '@/api';
import s from '@/components/Map/Map.module.css';
import { GraphHopperLimitError } from '@/services/api';

import { useStore } from './useStore';

export const useMap = () => {
export const useMap = ({ styles }: { styles: Dictionary<string> }) => {
const mapRef = useRef(null);
const { setRoute, setError } = useStore();
const { setRoute, setError, engine } = useStore();
const [isMapRendered, setIsMapRendered] = useState(false);

const initMap = useCallback(async () => {
// eslint-disable-next-line -- it's not a React hook
useGeographic();

const isRetina = Boolean(getRetinaMod());
const center = await getCurrentPosition();
const markersSource = new VectorSource();
const routeSource = new VectorSource();
const markersLayer = new VectorLayer({
source: markersSource,
style: startMarkerStyle,
});
const routeLayer = new VectorLayer({
source: routeSource,
style: {
'fill-color': 'rgba(255, 255, 255, 0.2)',
'stroke-color': 'red',
'stroke-width': 5,
},
});
const draw = new Draw({
source: markersSource,
type: 'Point',
style: {
'fill-color': 'none',
},
condition: (e) => {
let shouldBeDrawn = true;

map.forEachFeatureAtPixel(
e.pixel,
() => {
shouldBeDrawn = false;
},
{
layerFilter: (l) => l === markersLayer,
},
);
return shouldBeDrawn;
},
});

const modify = new Modify({ source: markersSource });
const snap = new Snap({ source: markersSource });

const map = new Map({
layers: [
// new TileLayer({
// source: new OSM(),
// }),
// new TileLayer({
// source: new XYZ({
// attributions: '&copy; MapTiler',
// url: `https://api.maptiler.com/maps/outdoor-v2/{z}/{x}/{y}${getRetinaMod()}.png?key=${process.env.MAPTILER_API_KEY}`,
// tilePixelRatio: isRetina ? 2 : 1,
// }),
// }),
new TileLayer({
source: new XYZ({
attributions: '&copy; OpenCycleMap',
url: `https://tile.thunderforest.com/cycle/{z}/{x}/{y}${getRetinaMod()}.png?apikey=${process.env.THUNDERFOREST_API_KEY}`,
tilePixelRatio: isRetina ? 2 : 1,
}),
}),
routeLayer,
markersLayer,
],
interactions: defaultInteractions().extend([draw, modify, snap]),
controls: [
new ScaleLine({
className: s.scaleLine,
}),
new Zoom({
className: s.zoom,
}),
new Attribution(),
],
target: mapRef.current ?? undefined,
view: new View({
center,
zoom: 15,
}),
});

map.once('rendercomplete', () => {
setIsMapRendered(true);
});

const unsubscribeFromDistance = useStore.subscribe(
(state) => state.distance,
(distance) => {
if (distance === 0) {
routeSource.clear();
markersSource.clear();
}
},
);

const renderRoute = async () => {
const markers = markersSource.getFeatures().sort(sortMarkersById);

if (markers.length < 2) return;

const coordinates = markers.map((f) =>
(f.getGeometry() as Point).getCoordinates(),
);

try {
const { engine } = useStore.getState();
const data = await fetchRoute({ engine, coordinates });
if (!data) return;

setRoute(data);
routeSource.clear();
routeSource.addFeatures(new GeoJSON().readFeatures(data.points));
} catch (err) {
if (err instanceof GraphHopperLimitError) {
setError(err.message);
}
}
};

markersSource.on('addfeature', (e) => {
const markers = markersSource.getFeatures();

if (markers.length === 1) {
e.feature?.setId('marker_start');
} else {
e.feature?.setId('marker_end');
e.feature?.setStyle(endMarkerStyle);
markers.forEach((m, i) => {
if (i !== 0 && i !== markers.length - 1) {
m.setId('marker_middle_' + i);
m.setStyle(interimMarkerStyle);
}

if (i === markers.length - 1) {
m.setId('marker_end');
m.setStyle(endMarkerStyle);
}
});
}
renderRoute();
});
modify.on('modifyend', renderRoute);

return unsubscribeFromDistance;
}, [setError, setRoute]);
const { cleanMap, renderRoute, initMap, onEvent } = useMemo(
() => new MapService(),
[],
);

useEffect(() => {
let unsubscribe = () => {};

(async () => {
try {
unsubscribe = await initMap();
} catch (err) {}
const map = await initMap({
container: mapRef?.current ?? undefined,
styles,
});
map?.once('rendercomplete', () => {
setIsMapRendered(true);
});
})();

return unsubscribe;
}, [initMap]);
return useStore.subscribe(
(state) => state.distance,
(distance) => {
if (distance === 0) {
cleanMap();
}
},
);
}, [cleanMap, initMap, styles]);

useEffect(() => {
const offEvent = onEvent(
'coordinatesChange',
async (coordinates: Coordinate[]) => {
if (!coordinates.length) {
return;
}

const data = await fetchRoute({ engine, coordinates });
setRoute(data);
try {
renderRoute(data);
} catch (err) {
if (err instanceof GraphHopperLimitError) {
setError(err.message);
}
}
},
);

return () => offEvent();
}, [engine, onEvent, renderRoute, setRoute, setError]);

return {
mapRef,
Expand Down
1 change: 1 addition & 0 deletions client/src/services/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './api';
export * from './map';
1 change: 1 addition & 0 deletions client/src/services/map/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { MapService } from './mapService';
Loading

0 comments on commit 319c6e7

Please sign in to comment.