aboutsummaryrefslogtreecommitdiff
path: root/frontend/ts/src/pages/map.ts
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/ts/src/pages/map.ts')
-rw-r--r--frontend/ts/src/pages/map.ts231
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])
+}