diff options
Diffstat (limited to 'frontend/ts/src/pages/map.ts')
-rw-r--r-- | frontend/ts/src/pages/map.ts | 231 |
1 files changed, 231 insertions, 0 deletions
diff --git a/frontend/ts/src/pages/map.ts b/frontend/ts/src/pages/map.ts new file mode 100644 index 0000000..b445f63 --- /dev/null +++ b/frontend/ts/src/pages/map.ts @@ -0,0 +1,231 @@ +import { h, Html } from 'lib/rx' +import * as rx from 'lib/rx' +import * as request from 'request' +import * as modal from 'ui/modal' +import * as route from 'route' +import { Map } from 'models/map' +import * as markerModel from 'models/marker' +import * as M from 'lib/leaflet' +import * as footer from 'pages/map/footer' +import * as markerView from 'pages/map/marker' +import * as markerForm from 'pages/map/markerForm' +import * as L from 'lib/loadable' +import * as layout from 'ui/layout' + +export function view(id: string): Html { + return rx.withState2<L.Loadable<Map>, L.Loadable<markerModel.Map>>( + [L.loading, L.loading], + (mapVar, markersVar) => { + request + .get<Map>(`/api/maps/${id}`) + .then(res => mapVar.update(_ => L.loaded(res))) + .catch(({ message }) => mapVar.update(_ => L.failure(message))) + + request + .get<Array<markerModel.Marker>>(`/api/markers?map=${id}`) + .then(res => markersVar.update(_ => L.loaded(markerModel.toMap(res)))) + .catch(({ message }) => markersVar.update(_ => L.failure(message))) + + return rx.map2( + [mapVar, markersVar], + (map, markers) => { + return layout.loadable( + L.map2([map, markers], (map, markers) => ({ map, markers })), + ({map, markers}) => h('div', + { className: 'g-Map' }, + withMap(map => mapView(id, map, markers)), + footer.view(map) + ) + ) + } + ) + } + ) +} + +function withMap(f: (map: M.Map) => Html): Html { + return rx.withState<M.Map | null>(null, mapVar => [ + h('div', + { + onmount: (element: HTMLElement) => { + const map = M.map(element, { + center: [30, 10], + zoom: 2.5, + attributionControl: false + }) + + map.addLayer(M.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png')) + + mapVar.update(_ => map) + } + } + ), + mapVar.map(map => map && f(map)) + ]) +} + +interface MarkerContext { + markerId: string + markerElem: M.FeatureGroup +} + +interface ErrorModal { + title: string + message: string +} + +function mapView(map_id: string, map: M.Map, markers: markerModel.Map): Html { + let lastUserAdded: markerModel.Marker | undefined + + return rx.withState3< + M.Pos | undefined, + MarkerContext | undefined, + ErrorModal | undefined + >( + [undefined, undefined, undefined], + (addModalVar, updateModalVar, errorModalVar) => { + map.addEventListener('click', (event: M.MapEvent) => addModalVar.update(_ => event.latlng)) + + const onClick = (m: MarkerContext) => updateModalVar.update(_ => m) + + const onMoveError = (message: string) => { + errorModalVar.update(_ => ({ + title: 'Erreur lors du déplacement du marqueur.', + message + })) + } + + const elements = M.featureGroup() + Object.values(markers).forEach(marker => { + const elem = addMarker({ marker, markers, map, onClick, onMoveError }) + elements.addLayer(elem) + }) + + // Fit bounds to elements + if (elements.getLayers().length > 0) { + map.fitBounds(elements.getBounds(), { padding: [ 100, 100 ] }) + } + + return [ + addModalVar.map(pos => { + if (pos !== undefined) { + return markerForm.view({ + lat: pos.lat, + lng: pos.lng, + colors: markerColors(markers), + lastUserAdded, + label: 'Ajouter', + req: (body: string) => request.post<markerModel.Marker>( + `/api/markers?map=${map_id}`, + body + ), + onSuccess: marker => { + addMarker({ marker, markers, map, onClick, onMoveError }) + addModalVar.update(_ => undefined) + markers[marker.id] = marker + lastUserAdded = marker + }, + onClose: () => addModalVar.update(_ => undefined) + }) + } + }), + updateModalVar.map(m => { + if (m !== undefined) { + const { markerId, markerElem } = m + const marker = markers[markerId] + return markerForm.view({ + lat: marker.lat, + lng: marker.lng, + marker, + colors: markerColors(markers), + label: 'Modifier', + req: (body: string) => request.put<markerModel.Marker>( + `/api/markers/${marker.id}`, + body + ), + onSuccess: marker => { + map.removeLayer(markerElem) + addMarker({ marker, markers, map, onClick, onMoveError }) + updateModalVar.update(_ => undefined) + markers[marker.id] = marker + lastUserAdded = marker + }, + onClose: () => updateModalVar.update(_ => undefined), + onDeleteMarker: () => { + map.removeLayer(markerElem) + delete markers[marker.id] + updateModalVar.update(_ => undefined) + } + }) + } + }), + errorModalVar.map(err => err && modal.error({ + header: err.title, + message: err.message, + onClose: () => errorModalVar.update(_ => undefined) + })) + ] + } + ) +} + +interface AddMarkerParams { + markers: markerModel.Map + marker: markerModel.Marker + map: M.Map + onClick: (m: MarkerContext) => void + onMoveError: (message: string) => void +} + +function addMarker({ markers, marker, map, onClick, onMoveError }: AddMarkerParams): M.FeatureGroup { + const elem = markerView.create({ + marker, + onMove: markerElem => { + const pos = markerElem.getLatLng() + const newMarker = structuredClone(marker) + newMarker.lat = pos.lat + newMarker.lng = pos.lng + updateMarker(newMarker) + .then(m => markers[marker.id] = m) + .catch(({ message }) => { + markerElem.setLatLng({ lat: marker.lat, lng: marker.lng }) + onMoveError(message) + }) + }, + onClick: (markerElem: M.FeatureGroup) => onClick({ markerId: marker.id, markerElem }) + }) + + map.addLayer(elem) + + return elem +} + +function updateMarker(m: markerModel.Marker): Promise<markerModel.Marker> { + const payload = { + lat: m.lat, + lng: m.lng, + color: m.color, + name: m.name, + description: m.description, + icon: m.icon, + radius: m.radius + } + + return request.put(`/api/markers/${m.id}`, JSON.stringify(payload)) +} + +function markerColors(markers: markerModel.Map): Array<string> { + const frequencies: { [key: string]: number } = {} + Object.values(markers).forEach(m => { + if (m.color in frequencies) { + frequencies[m.color] += 1 + } else { + frequencies[m.color] = 1 + } + }) + let colors = Object.keys(frequencies).map(key => [key, frequencies[key]]) + // @ts-ignore + colors.sort((a, b) => b[1] - a[1]) + // @ts-ignore + return colors.map(c => c[0]) +} |