'use strict'; import { CRS, DivIcon, ImageOverlay, LatLng, LatLngBounds, Map, Marker, Popup, } from './leaflet/leaflet.js'; let cookies = {} let map = null; let active = null; const markers = {} window.addEventListener('DOMContentLoaded', async (event) => { try { await makeApiRequest('GET', '/'); cookies = get_cookies(); const settings = JSON.parse(atob(cookies['server_settings'])); document.querySelector('head > title') .textContent = settings['site_name']; document.getElementById('banner-sitename') .textContent = settings['site_name']; document.getElementById('banner-userid') .textContent = cookies['username']; map = setupMap('map', 'static/karta_plan3.png'); makeApiRequest('GET', '/points') .then((data) => { for(const [id, values] of Object.entries(data)) { const pointData = {id: id, position: new LatLng(values.latitude, values.longitude), placeName: values.placeName, personName: values.personName, personRole: values.personRole}; if(values.hasOwnProperty('comment')) { pointData.comment = values.comment; } drawMarker(pointData); } }); const create_button = document.getElementById('create-workspace'); if(cookies['admin']) { create_button.textContent = 'Skapa arbetsplatser'; create_button.addEventListener('click', (event) => { create_button.classList.toggle('cancel'); if(create_button.classList.contains('cancel')) { create_button.textContent = 'Sluta skapa arbetsplatser'; map.on('click', createMarker); } else { create_button.textContent = 'Skapa arbetsplatser'; map.off('click', createMarker); } }); } else { create_button.remove(); } const export_button = document.getElementById('export-workspaces'); if(cookies['export']) { export_button.addEventListener('click', (event) => { makeApiRequest('GET', '/export').then((result) => { // Have to pass a name, but it won't be used so passing '' const file = new File([result.csv_data], ''); const url = window.URL.createObjectURL(file); const a = document.createElement('a'); a.href = url; a.download = `export-dsv-karta.tsv`; document.body.appendChild(a); a.click(); a.remove(); }); }); } else { export_button.remove(); } const toggle_button = document.getElementById('toggle-labels'); toggle_button.addEventListener('click', (event) => { localStorage.setItem('label-visibility', !getVisibility()); updateLabels(); }); const search_button = document.getElementById('search-button'); search_button.addEventListener('click', (event) => { const search_term = document.getElementById('search-term').value; if(!search_term) { clearSearchResults(); return; } clearSearchResults(true); getSearchResults(search_term) .then((result_ids) => prepareResultElements(result_ids)) .then((result_elements) => showSearchResults(result_elements)); }); document.querySelector('body').removeAttribute('style'); } catch(e) { if(e.message === 'access denied') { deny_access(); return; } throw e; } }); function setupMap(element_id, map_image) { const top_left = new LatLng(10, 0); const bottom_right = new LatLng(0, 10); const bounds = new LatLngBounds(top_left, bottom_right); let zoom = 7; const stored_zoom = localStorage.getItem('zoom-level'); if(stored_zoom) { zoom = Number(stored_zoom); } let center = bounds.getCenter(); const latitude = localStorage.getItem('view-latitude'); const longitude = localStorage.getItem('view-longitude'); if(latitude) { center = new LatLng(Number(latitude), Number(longitude)); } const map = new Map(element_id, { center: center, maxBounds: bounds.pad(0.5), zoom: zoom, zoomSnap: 0.1}); const background = new ImageOverlay(map_image, bounds); background.addTo(map); map.on('zoomend', () => { localStorage.setItem('zoom-level', map.getZoom()); }); map.on('moveend', (event) => { const newPosition = map.getCenter(); localStorage.setItem('view-latitude', newPosition.lat); localStorage.setItem('view-longitude', newPosition.lng); }); map.on('click', (event) => { clearActive(); }); return map; } function createMarker(event) { const form = document.createElement('div'); form.appendChild(document.getElementById('pin-form') .content.cloneNode(true)); form.querySelector('#save').addEventListener('click', () => { const markerData = { id: crypto.randomUUID(), position: event.latlng, placeName: form.querySelector('#workspace-name').value, personName: form.querySelector('#person-name').value, personRole: form.querySelector('#person-role').value, comment: form.querySelector('#comment').value } saveMarker(markerData); drawMarker(markerData); map.closePopup(); }); const deleteButton = form.querySelector('#delete'); deleteButton.parentNode.removeChild(deleteButton); map.openPopup(form, event.latlng); form.querySelector('#workspace-name').focus(); } function drawMarker(data) { const markerId = data.id; const position = data.position; const placeName = data.placeName; const personName = data.personName; const personRole = data.personRole; let comment = ''; if(cookies['export']) { comment = data.comment; } const label = document.createElement('div'); let draggable = false; if(cookies['admin']) { draggable = true; } label.appendChild(document.getElementById('pin-template') .content.cloneNode(true)); const icon_hack = new DivIcon({ className: 'map-icon', iconSize: [20, 20], html: label }); const marker = new Marker(position, { icon: icon_hack, title: placeName, draggable: draggable }); const popup_div = document.createElement('div'); if(cookies['admin']) { popup_div.appendChild(document.getElementById('pin-form') .content.cloneNode(true)); const popup_placename = popup_div.querySelector('#workspace-name'); const popup_personname = popup_div.querySelector('#person-name'); const popup_personrole = popup_div.querySelector('#person-role'); const popup_comment = popup_div.querySelector('#comment'); popup_placename.value = placeName; popup_personname.value = personName; popup_personrole.value = personRole; popup_comment.value = comment; popup_div.querySelector('#save').addEventListener('click', () => { markers[markerId].placeName = popup_placename.value; markers[markerId].personName = popup_personname.value; markers[markerId].personRole = popup_personrole.value; markers[markerId].comment = popup_comment.value; saveMarker({id: markerId, position: markers[markerId].marker.getLatLng(), placeName: popup_placename.value, personName: popup_personname.value, personRole: popup_personrole.value, comment: popup_comment.value}); map.closePopup(); updateLabel(marker); }); popup_div.querySelector('#delete').addEventListener('click', () => { deleteMarker(markerId); delete markers[markerId]; map.removeLayer(marker); }); marker.on('popupopen', (event) => { if(!popup_placename.value) { popup_placename.focus(); } else { popup_personname.focus(); } }); marker.on('popupclose', (event) => { if(!markers[markerId]) { // Don't act on deleted markers return; } popup_placename.value = markers[markerId].placeName; popup_personname.value = markers[markerId].personName; popup_personrole.value = markers[markerId].personRole; popup_comment.value = markers[markerId].comment; }); marker.on('move', (event) => { saveMarker({id: markerId, position: markers[markerId].marker.getLatLng(), placeName: markers[markerId].placeName, personName: markers[markerId].personName, personRole: markers[markerId].personRole, comment: markers[markerId].comment, }); }); } else { popup_div.appendChild(document.getElementById('pin-info') .content.cloneNode(true)); popup_div.querySelector('.workspace-name').textContent = placeName; popup_div.querySelector('.person-name').textContent = personName; const role_element = popup_div.querySelector('.person-role'); if(personRole) { role_element.textContent = personRole; } else { role_element.remove(); } if(!comment) { popup_div.querySelector('.comment').remove(); } else { popup_div.querySelector('.comment-text').textContent = comment; } } marker.on('popupopen', (event) => { if(active && marker !== active.marker) { clearActive(); } }); const result_template = document.getElementById('result-template') .content.cloneNode(true); const search_element = result_template.querySelector('.search-result'); search_element.addEventListener('click', (event) => { setActive(marker, search_element); map.flyTo(marker.getLatLng()); }); search_element.addEventListener('dblclick', (event) => { setActive(marker, search_element); map.flyTo(marker.getLatLng()); marker.openPopup(); }); markers[markerId] = { marker: marker, search_element: search_element, placeName: placeName, personName: personName, personRole: personRole, comment: comment }; marker.id = markerId; const popup = new Popup(); popup.setContent(popup_div); marker.bindPopup(popup); marker.addTo(map); updateLabel(marker); } function updateLabels(overrides) { for(const [id, values] of Object.entries(markers)) { updateLabel(values.marker, overrides); } } function updateLabel(marker, overrides) { if(!overrides) { overrides = {}; } const placeName = markers[marker.id].placeName; const personName = markers[marker.id].personName; const personRole = markers[marker.id].personRole; const comment = markers[marker.id].comment; const label = marker.getElement().querySelector('.map-flex'); const text_label = label.querySelector('.label'); let visible = getVisibility(); if(overrides.visible !== undefined) { visible = overrides.visible; } let icon_src = './static/pin-red.svg'; let tooltip = personName; if(!personName) { icon_src = './static/pin-yellow.svg'; tooltip = placeName; } let z_offset = 0; if(overrides.active) { icon_src = './static/pin-blue.svg'; z_offset = 1000; } let dim = false; if(overrides.dim !== undefined) { dim = overrides.dim; } if(dim) { label.classList.add('dim'); icon_src = './static/pin-gray.svg'; } else { label.classList.remove('dim'); } marker.setZIndexOffset(z_offset); label.querySelector('img').setAttribute('src', icon_src); marker.getElement().setAttribute('title', tooltip); label.querySelector('.location').textContent = placeName; label.querySelector('.person').textContent = personName; // Avoiding use of .toggle() here so we guarantee consistency // even if other actions have modified individual labels if(visible) { text_label.classList.remove('no-label'); } else { text_label.classList.add('no-label'); } const search_element = markers[marker.id].search_element; search_element.querySelector('.location').textContent = placeName; search_element.querySelector('.person').textContent = personName; const role_element = search_element.querySelector('.role'); role_element.textContent = personRole; if(!role_element.textContent) { role_element.classList.add('hidden'); } else { role_element.classList.remove('hidden'); } const comment_element = search_element.querySelector('.comment'); comment_element.textContent = comment; if(!comment_element.textContent) { comment_element.classList.add('hidden'); } else { comment_element.classList.remove('hidden'); } } function getVisibility() { // This hack deals with localStorage only storing strings: return localStorage.getItem('label-visibility') === 'true'; } function clearSearchResults(doing_search) { const results_div = document.getElementById('search-results'); while(results_div.firstChild) { results_div.removeChild(results_div.lastChild); } if(doing_search) { const spinner = document.createElement('img'); spinner.setAttribute('src', './static/spinner.svg'); spinner.id = 'search-spinner'; results_div.appendChild(spinner); } updateLabels(); return results_div; } async function getSearchResults(search_term) { const lowercase_term = search_term.toLowerCase(); const result_ids = []; for(const [id, values] of Object.entries(markers)) { if(values.placeName.toLowerCase().includes(lowercase_term) || values.personName.toLowerCase().includes(lowercase_term)) { result_ids.push(id); } } return result_ids; } async function prepareResultElements(result_ids) { const result_objects = []; for(const result_id of result_ids) { result_objects.push(markers[result_id]); } result_objects.sort((e1, e2) => { const person1 = e1.personName.toLowerCase(); const person2 = e2.personName.toLowerCase(); if(person1 && !person2) { return -1; } if(person2 && !person1) { return 1; } if(person1 && person2) { return person1.localeCompare(person2); } const location1 = e1.placeName.toLowerCase(); const location2 = e2.placeName.toLowerCase(); if(location1 && !location2) { return -1; } if(location2 && !location1) { return 1; } if(location1 && location2) { return location1.localeCompare(location2); } return 0; }); return result_objects; } async function showSearchResults(result_objects) { const results_div = clearSearchResults(); updateLabels({dim: true}); result_objects.forEach((result) => { results_div.appendChild(result.search_element); updateLabel(result.marker); }); } function setActive(marker, result_element) { clearActive(); active = {marker: marker, result: result_element}; updateLabel(marker, {active: true, visible: true}); result_element.classList.add('active'); } function clearActive() { if(active) { updateLabel(active.marker); active.result.classList.remove('active'); active = null; } } function deny_access() { const body = document.querySelector('body'); const main = document.querySelector('main'); const sidebar = document.getElementById('sidebar'); const header = document.getElementById('banner-sitename'); const message = document.createElement('p'); body.removeChild(main); body.removeChild(sidebar); header.textContent = 'Access denied'; message.textContent = 'You do not have access to this site.'; message.id = 'access-denied'; body.appendChild(message); body.removeAttribute('style'); } function get_cookies() { var out = new Object(); const cookies = document.cookie.split('; '); cookies.forEach((cookie) => { const temp = cookie.split('='); const name = temp[0]; const value = temp.slice(1).join('='); out[name] = value; }); return out; } function login() { const current_path = window.location.pathname; return window.location.replace('/api/login?return=' + current_path); } async function saveMarker(data) { const request_body = {id: data.id, placeName: data.placeName, personName: data.personName, latitude: data.position.lat, longitude: data.position.lng, personRole: data.personRole, comment: data.comment}; makeApiRequest('PUT', '/point', request_body); } async function deleteMarker(markerId) { makeApiRequest('DELETE', '/point', {'id': markerId}); } async function makeApiRequest(method, path, body) { const data = {'method': method, 'headers': {'Content-Type': 'application/json'}}; if(method != 'GET') { data['body'] = JSON.stringify(body); } const request = new Request('/api' + path, data); return fetch(request) .then((response) => { if(response.status === 403) { const access = cookies['access']; if(access === 'denied') { throw new Error('access denied'); } login(); } return response.json(); }, (error) => { alert('A network error has occurred. Please reload the page.'); }); }