const DEFAULT_ZOOM = 14; const DEFAULT_COORDINATES = [38.59104623572979, -9.130882470026634]; const ELEM_ID_MAP = "map"; const ELEM_ID_BTN_SHAPE_CREATE = "shape-create"; const ELEM_ID_BTN_SHAPE_DELETE = "shape-delete"; const ELEM_ID_BTN_SHAPE_DELETE_VERTEX = "shape-delete-vertex"; const ELEM_ID_BTN_SHAPE_BURNED = "shape-kind-burned"; const ELEM_ID_BTN_SHAPE_UNBURNED = "shape-kind-unburned"; const ELEM_ID_BTN_SHAPES_UPDATE = "shapes-update"; const SHAPE_KIND_UNBURNED = "unburned"; const SHAPE_KIND_BURNED = "burned"; /** * A location log * @typedef {Object} LocationMarker * @property {number} timestamp * @property {number} latitude * @property {number} longitude * @property {number} accuracy - Accuracy in meters * @property {number} heading - Compass heading in degress [0, 360] */ /** * A shape point * @typedef {Object} ShapePoint * @property {number} latitude * @property {number} longitude */ /** * A shape descriptor * @typedef {Object} ShapeDescriptor * @property {string} kind * @property {[]ShapePoint} points */ /** * A picture descriptor * @typedef {Object} PictureDescriptor * @property {string} filename * @property {number} latitude * @property {number} longitude */ /** * A shape * @typedef {Object} Shape * @property {string} kind * @property {[]ShapePoint} points * @property {[]Object} layers - leaflet layers * @property {number} point_insert_idx - index to start inserting points */ function lib_setup_handler_onclick(elementId, handler) { document.getElementById(elementId).onclick = handler } function lib_setup_map() { var map = L.map(ELEM_ID_MAP).setView(DEFAULT_COORDINATES, DEFAULT_ZOOM); L.Icon.Default.imagePath = "/static/"; L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { maxNativeZoom: 19, maxZoom: 25, attribution: '© OpenStreetMap' }).addTo(map); return map; } /** * Fetch location logs * @return {Promise} */ async function lib_fetch_location_logs() { // const burned = [ // [38.592177702929426, -9.145557060034113], // [38.58385651421202, -9.134116290522673], // [38.587516574932266, -9.134999747627804], // [38.59442184182009, -9.13809184749576], // [38.596734957715675, -9.138621921758839], // ]; // // const unburned = [ // [38.598388277527036, -9.135874396116632], // [38.589731317901276, -9.149692038446165], // [38.58043902375093, -9.138619879692945], // [38.591568658478, -9.12070962376425], // ]; // // const location_logs = [] // for (const point of burned.concat(unburned)) { // console.log(point) // location_logs.push({ // latitude: point[0], // longitude: point[1], // accuracy: 5.8, // timestamp: 0, // heading: 0, // }) // } // return Promise.resolve(location_logs); const response = await fetch("/api/location"); return response.json(); } /** * Fetch shape descriptors * @return {Promise} */ async function lib_fetch_shape_descriptors() { const response = await fetch("/api/shapes"); return response.json(); } /** * Fetch picture descriptors * @return {Promise} */ async function lib_fetch_picture_descriptors() { const response = await fetch("/api/pictures"); return response.json(); } function lib_picture_descriptor_url(picture_descriptor) { const picture_url = `/picture/${picture_descriptor.filename}` return picture_url; } function lib_add_location_logs_to_map(map, locations) { for (const location of locations) { const len = 0.0002; const lat = Math.sin((location.heading + 90) * 2 * Math.PI / 360) * len; const lon = Math.cos((location.heading - 90) * 2 * Math.PI / 360) * len; const line_coords = [ [location.latitude, location.longitude], [location.latitude + lat, location.longitude + lon], ]; L.circle([location.latitude, location.longitude], { radius: location.accuracy }) .bindPopup(`${location.heading}`) .addTo(map); L.polyline(line_coords, { color: 'blue' }).addTo(map) } } function lib_shape_color_for_kind(kind) { if (kind == SHAPE_KIND_BURNED) return 'red'; if (kind == SHAPE_KIND_UNBURNED) return 'green'; return 'orange'; } function lib_shape_create_empty() { return { kind: SHAPE_KIND_UNBURNED, points: [], layers: [], point_insert_idx: 0, }; } function lib_shape_create_from_descriptor(desc) { return { kind: desc.kind, points: desc.points, layers: [], point_insert_idx: desc.points.length, }; } function page_editor__on_shape_create(state) { const shape = lib_shape_create_empty(); state.shapes.push(shape); page_editor__ui_shape_select(state, shape); } function page_editor__on_shape_delete(state) { if (state.selected_shape == null) return; page_editor__ui_shape_remove(state, state.selected_shape); state.shapes.splice(state.shapes.indexOf(state.selected_shape), 1); state.selected_shape = null; } function page_editor__on_shape_delete_vertex(state) { if (state.delete_selected_vertex_fn == null) return; state.delete_selected_vertex_fn(); } function page_editor__on_shape_kind_unburned(state) { if (state.selected_shape == null) return; state.selected_shape.kind = SHAPE_KIND_UNBURNED; page_editor__ui_shape_add(state, state.selected_shape); } function page_editor__on_shape_kind_burned(state) { if (state.selected_shape == null) return; state.selected_shape.kind = SHAPE_KIND_BURNED; page_editor__ui_shape_add(state, state.selected_shape); } function page_editor__on_shapes_update(state) { const shape_descriptors = []; for (const shape of state.shapes) { if (shape.points.length < 3) continue; const points = []; for (const point of shape.points) { points.push({ latitude: point.latitude, longitude: point.longitude, }); } shape_descriptors.push({ kind: shape.kind, points: points, }); } fetch("/api/shapes", { method: "POST", body: JSON.stringify(shape_descriptors), }).then(() => { alert("updated"); window.location.reload(); }).catch((e) => { alert(`failed to update: ${e}`); window.location.reload(); }); } function page_editor__on_map_click(state, ev) { console.log("clicked on map"); if (state.selected_shape == null) return; state.selected_shape.points.splice(state.selected_shape.point_insert_idx, 0, { latitude: ev.latlng.lat, longitude: ev.latlng.lng, }); state.selected_shape.point_insert_idx += 1; page_editor__ui_shape_add(state, state.selected_shape); } function page_editor__setup_handlers(state) { lib_setup_handler_onclick(ELEM_ID_BTN_SHAPE_CREATE, () => page_editor__on_shape_create(state)); lib_setup_handler_onclick(ELEM_ID_BTN_SHAPE_DELETE, () => page_editor__on_shape_delete(state)); lib_setup_handler_onclick(ELEM_ID_BTN_SHAPE_DELETE_VERTEX, () => page_editor__on_shape_delete_vertex(state)); lib_setup_handler_onclick(ELEM_ID_BTN_SHAPE_UNBURNED, () => page_editor__on_shape_kind_unburned(state)); lib_setup_handler_onclick(ELEM_ID_BTN_SHAPE_BURNED, () => page_editor__on_shape_kind_burned(state)); lib_setup_handler_onclick(ELEM_ID_BTN_SHAPES_UPDATE, () => page_editor__on_shapes_update(state)); state.map.on('click', (ev) => page_editor__on_map_click(state, ev)); } function page_editor__ui_shape_remove(state, shape) { if (shape == null) return; for (const layer of shape.layers) { state.map.removeLayer(layer); layer.remove(); } shape.layers = []; } function page_editor__ui_shape_select(state, shape) { const prev_shape = state.selected_shape; state.selected_shape = shape; page_editor__ui_shape_add(state, shape); if (prev_shape != null) page_editor__ui_shape_add(state, prev_shape); } function page_editor__ui_shape_add(state, shape) { if (shape == null) return; page_editor__ui_shape_remove(state, shape); const selected = state.selected_shape == shape; const color = lib_shape_color_for_kind(shape.kind); const positions = []; for (var i = 0; i < shape.points.length; i += 1) { const point = shape.points[i]; const highlight_point = (shape.point_insert_idx - 1 == i) && selected; const coords = [point.latitude, point.longitude]; const circle_color = highlight_point ? 'blue' : 'red'; const circle_idx = i; console.assert(point.latitude != null, "invalid point latitude") console.assert(point.longitude != null, "invalid point longitude") const remove_circle = () => { shape.points.splice(circle_idx, 1); shape.point_insert_idx = circle_idx; page_editor__ui_shape_add(state, shape); }; const update_insert_idx = () => { shape.point_insert_idx = circle_idx + 1; page_editor__ui_shape_add(state, shape); }; if (highlight_point) state.delete_selected_vertex_fn = remove_circle; positions.push(coords); const circle = L.circle(coords, { radius: state.vertex_radius, color: circle_color, bubblingMouseEvents: false }) .on('click', (e) => { if (e.originalEvent.shiftKey) { remove_circle(); } else { update_insert_idx(); } }) .on('contextmenu', () => remove_circle()) .addTo(state.map); shape.layers.push(circle); if (selected) { const tooltip = L.tooltip(coords, { content: `${i}` }) .addTo(state.map); shape.layers.push(tooltip); } } if (positions.length >= 3) { const opacity = state.selected_shape == shape ? 0.2 : 0.04; const select = () => page_editor__ui_shape_select(state, shape); const poly = L.polygon(positions, { color: color, fillOpacity: opacity }) .on('click', (ev) => { if (ev.originalEvent.shiftKey) { L.DomEvent.stopPropagation(ev); select(); } }) .on('dblclick', (ev) => { L.DomEvent.stopPropagation(ev); select(); }) .addTo(state.map); shape.layers.push(poly); } } async function page_editor__main() { const map = lib_setup_map(); const locations = await lib_fetch_location_logs(); const shape_descriptors = await lib_fetch_shape_descriptors(); const state = { map: map, locations: locations, shapes: [], selected_shape: null, delete_selected_vertex_fn: null, vertex_radius: 15, }; window.state = state; // to allow access from the console page_editor__setup_handlers(state); lib_add_location_logs_to_map(state.map, state.locations); const vertex_radius_slider = document.getElementById("shape-vertex-radius"); vertex_radius_slider.addEventListener("change", () => { state.vertex_radius = vertex_radius_slider.value; page_editor__ui_shape_add(state, state.selected_shape); }); for (const descriptor of shape_descriptors) { const shape = lib_shape_create_from_descriptor(descriptor); state.shapes.push(shape); page_editor__ui_shape_add(state, shape); } } function page_index__poly_create_from_shape_descriptor(map, shape_descriptor) { const color = lib_shape_color_for_kind(shape_descriptor.kind); const points = [] for (const point of shape_descriptor.points) { points.push([point.latitude, point.longitude]); } L.polygon(points, { color: color }).addTo(map); } function page_index__create_image_popup(picture_descriptor) { const e = document.getElementById("image-frame"); if (e != null) e.remove(); const d = document.createElement("div"); d.id = "image-frame"; const i = document.createElement("img"); i.src = lib_picture_descriptor_url(picture_descriptor); d.onclick = () => { d.remove(); }; d.appendChild(i); document.body.appendChild(d); } function page_index__add_picture_descriptor_to_map(map, picture_descriptor) { L.marker([picture_descriptor.latitude, picture_descriptor.longitude]) .on('click', () => { page_index__create_image_popup(picture_descriptor); }) .addTo(map) } async function page_index__main() { const map = lib_setup_map(); const [shape_descriptors, picture_descriptors] = await Promise.all([ lib_fetch_shape_descriptors(), lib_fetch_picture_descriptors(), ]); for (const descriptor of shape_descriptors) { page_index__poly_create_from_shape_descriptor(map, descriptor); } for (const descriptor of picture_descriptors) { page_index__add_picture_descriptor_to_map(map, descriptor); } setTimeout(() => { console.log("create div"); }, 1000); }