Erik Thuning a02ae4a587 Added the ability to export map data to TSV
Exporting is allowed for anyone with export permission or above.
Pseudo-versions have been bumped in order to ensure no cache issues.
2025-09-12 17:15:56 +02:00

594 lines
19 KiB
JavaScript

'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.');
});
}