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>( [L.loading, L.loading], (mapVar, markersVar) => { request .get(`/api/maps/${id}`) .then(res => mapVar.update(_ => L.loaded(res))) .catch(({ message }) => mapVar.update(_ => L.failure(message))) request .get>(`/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(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( `/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( `/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 { 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 { 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]) }