aboutsummaryrefslogtreecommitdiffstats
path: root/main.js
diff options
context:
space:
mode:
Diffstat (limited to 'main.js')
-rw-r--r--main.js595
1 files changed, 594 insertions, 1 deletions
diff --git a/main.js b/main.js
index 88ac070..d9df4c0 100644
--- a/main.js
+++ b/main.js
@@ -126,6 +126,65 @@ const view = new View({
constrainResolution: false,
});
+const age_filter_settings = {
+ active: false,
+ type: 'relative',
+ operator: '<=',
+ quantity: 1,
+ unit: 'y',
+ show_unknown: false,
+ get_relative_date: function(quantity, unit) {
+ if (quantity == null || isNaN(quantity) || unit == null) {
+ return null;
+ }
+ /* use today noon localtime to avoid issues due to DST when substracting dates */
+ const d = new Date();
+ d.setHours(12, 0, 0, 0);
+ switch (unit) {
+ case 'd':
+ d.setDate(d.getDate() - quantity);
+ break;
+ case 'w':
+ d.setDate(d.getDate() - 7 * quantity);
+ break;
+ case 'm':
+ d.setMonth(d.getMonth() - quantity);
+ break;
+ case 'y':
+ d.setFullYear(d.getFullYear() - quantity);
+ break;
+ default:
+ return null;
+ }
+ return d;
+ },
+ _min_age: null,
+ _max_age: null,
+ _date_to_age: function(d) {
+ if (d == null) {
+ return null;
+ }
+ /* number of days since 1970-01-01; take both dates at 00:00:00.0 UTC */
+ return Math.floor(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())/86_400_000);
+ },
+ setup_minmax: function() {
+ this._min_age = this._max_age = null;
+ switch (this.type) {
+ case 'relative': {
+ const date = this.get_relative_date(this.quantity, this.unit);
+ const prop = {'<=':'_min_age', '>=':'_max_age'}[this.operator];
+ this[prop] = this._date_to_age(date);
+ break;
+ }
+ case 'interval': {
+ this._min_age = this._date_to_age(this.from);
+ this._max_age = this._date_to_age(this.to);
+ break;
+ }
+ }
+ },
+};
+
let baseMapLayer = 'topowebb_nedtonad';
(function() {
const params = new URLSearchParams(window.location.hash.substring(1));
@@ -165,6 +224,50 @@ let baseMapLayer = 'topowebb_nedtonad';
baseMapLayer = params.get('basemap');
}
baseMapSource.setUrl(`https://minkarta.lantmateriet.se/map/topowebbcache?LAYER=${encodeURIComponent(baseMapLayer)}`);
+
+ if (params.has('age-filter')) {
+ (function(param) {
+ if (param === '') {
+ return;
+ }
+ const m0 = /^([ +\-]?)([0-9]+)([dwmy])$/.exec(param);
+ if (m0 != null) {
+ age_filter_settings.type = 'relative';
+ age_filter_settings.operator = (m0[1] === ' ' || m0[1] === '+' || m0[1] === '') ? '>='
+ : m0[1] === '-' ? '<='
+ : null;
+ age_filter_settings.quantity = parseInt(m0[2], 10);
+ age_filter_settings.unit = m0[3];
+ age_filter_settings.setup_minmax();
+ age_filter_settings.active = true;
+ return;
+ }
+ const m1 = /^([0-9]{8})-([0-9]{8})$/.exec(param); /* YYYYMMDD */
+ if (m1 != null) {
+ const parse_date = (m) => new Date(
+ parseInt(m.slice(0,4), 10),
+ parseInt(m.slice(4,6), 10)-1,
+ parseInt(m.slice(6,8), 10),
+ 12 /* use 12:00:00.0 like for the <input type="date"> */
+ );
+ age_filter_settings.type = 'interval';
+ age_filter_settings.from = parse_date(m1[1]);
+ age_filter_settings.to = parse_date(m1[2]);
+ age_filter_settings.setup_minmax();
+ age_filter_settings.active = true;
+ return;
+ }
+ console.log(`Ignoring invalid value for \'age-filter\' parameter: ${param}`);
+ })(params.get('age-filter'));
+ }
+ if (params.has('show-unknown-age')) {
+ const param = params.get('show-unknown-age');
+ if (param === '0') {
+ age_filter_settings.show_unknown = false;
+ } else if (param === '1') {
+ age_filter_settings.show_unknown = true;
+ }
+ }
})();
@@ -192,6 +295,16 @@ const container = document.getElementById('map-control-container');
const info_backdrop = document.createElement('div');
container0.appendChild(info_backdrop);
info_backdrop.id = 'info-modal-backdrop';
+
+ const age_filter = document.createElement('div');
+ age_filter.id = 'age-filter-modal';
+ age_filter.classList.add('modal');
+ age_filter.setAttribute('tabindex', '-1');
+ age_filter.setAttribute('aria-hidden', 'true');
+ container0.appendChild(age_filter);
+ const age_filter_backdrop = document.createElement('div');
+ age_filter_backdrop.id = 'age-filter-modal-backdrop';
+ container0.appendChild(age_filter_backdrop);
})();
/* zoom in/out */
@@ -280,6 +393,7 @@ if (window.location === window.parent.location) {
const buttons = Object.fromEntries([
{id: 'layer-selection', title: 'Lagerval', bi: 'stack'},
{id: 'map-legend', title: 'Teckenförklaring', bi: 'list-task'},
+ {id: 'age-filter', title: 'Filtrera objekt efter ålder', bi: 'clock-history'},
].map(function(x) {
const div = document.createElement('div');
menu.appendChild(div);
@@ -302,6 +416,9 @@ if (window.location === window.parent.location) {
Object.keys(buttons).forEach(function(id) {
const panel = document.getElementById(id + '-panel');
+ if (panel == null) {
+ return;
+ }
const btn = buttons[id];
btn.onclick = function(event) {
if (btn.getAttribute('aria-expanded') === 'true') {
@@ -4426,7 +4543,21 @@ const [mapLayers, featureOverlayLayer] = (function() {
declutter: false,
visible: isVisible(k),
style: function(feature, resolution) {
- const style = styles[k + '.' + feature.getProperties().layer];
+ const properties = feature.getProperties();
+ if (age_filter_settings.active) {
+ /* TODO avoid doing this checks for each feature; instead, set up a
+ * different style function if age_filter_settings.active */
+ const age = properties.age;
+ if (age == null) {
+ if (!age_filter_settings.show_unknown) {
+ return null;
+ }
+ } else if ((age_filter_settings._min_age !== null && age < age_filter_settings._min_age) ||
+ (age_filter_settings._max_age !== null && age > age_filter_settings._max_age)) {
+ return null;
+ }
+ }
+ const style = styles[k + '.' + properties.layer];
if (!Array.isArray(style)) {
return style;
} else {
@@ -4905,6 +5036,468 @@ const infoMetadataAccordions = [];
})();
})();
+/* age filter panel */
+(function() {
+ const panel = document.getElementById('age-filter-modal');
+
+ const dialog_setup = (function() {
+ const dialog = document.createElement('div');
+ dialog.classList.add('modal-dialog', 'modal-dialog-centered');
+ panel.appendChild(dialog);
+
+ const content = document.createElement('div');
+ content.classList.add('modal-content');
+ dialog.appendChild(content);
+
+ const header = document.createElement('div');
+ header.classList.add('modal-header');
+ content.appendChild(header);
+
+ const title = document.createElement('div');
+ title.classList.add('h4', 'm-0');
+ header.appendChild(title);
+ title.innerHTML = 'Filtrera objekt efter ålder';
+
+ const btn_close = document.createElement('button');
+ btn_close.classList.add('btn-close');
+ btn_close.type = 'button';
+ btn_close.title = 'Stäng';
+ btn_close.setAttribute('aria-label', btn_close.title);
+ btn_close.setAttribute('data-bs-dismiss', 'modal');
+ header.appendChild(btn_close);
+
+ const body = document.createElement('div');
+ body.classList.add('modal-body');
+ content.appendChild(body);
+
+ const p = document.createElement('p');
+ p.classList.add('small', 'text-muted', 'age-filter-infotext');
+ body.appendChild(p);
+ p.appendChild(document.createTextNode('Här kan du filtrera bort objekt som är äldre eller ' +
+ 'yngre än ett valt datum. I nulaget gäller filtret endast objekt inom '));
+ ['mineralrättigheter', 'skogsbruk', 'vindbruk'].forEach(function(l, idx, arr) {
+ if (idx > 0) {
+ p.appendChild(document.createTextNode(idx < arr.length-1 ? ', ' : ' eller '));
+ }
+ const i = document.createElement('i');
+ i.innerHTML = l;
+ p.appendChild(i);
+ });
+ p.appendChild(document.createTextNode(' skikter.'));
+
+ const form = document.createElement('form');
+ form.action = '#';
+ form.id = 'age-filter-form';
+ body.appendChild(form);
+
+ const btn_group = document.createElement('div');
+ btn_group.classList.add('btn-group');
+ btn_group.setAttribute('role', 'group');
+ btn_group.setAttribute('aria-label', 'Välj filtreringstyp');
+ form.appendChild(btn_group);
+
+ const p2 = document.createElement('p');
+ form.appendChild(p2);
+ p2.innerHTML = 'Visa endast objekt med datum:';
+
+ const type_choices = Object.fromEntries([
+ { id: 'relative', text: 'Relativt datum' },
+ { id: 'interval', text: 'Intervall' },
+ ].map(function(x) {
+ const radio = document.createElement('input');
+ radio.classList.add('btn-check');
+ radio.type = 'radio';
+ radio.name = 'age-filter-type';
+ radio.id = 'age-filter-type-' + x.id;
+ radio.required = true;
+ radio.setAttribute('aria-expanded', 'false');
+ radio.setAttribute('autocomplete', 'off');
+ btn_group.appendChild(radio);
+
+ const lbl = document.createElement('label');
+ lbl.classList.add('btn', 'btn-primary');
+ lbl.setAttribute('for', radio.id);
+ lbl.appendChild(document.createTextNode(x.text));
+ btn_group.appendChild(lbl);
+
+ const div = document.createElement('div');
+ div.classList.add('d-none', 'age-filter-settings');
+ div.setAttribute('aria-hidden', 'true');
+ form.appendChild(div);
+
+ return [x.id, { radio: radio, div: div } ];
+ }));
+
+ Object.values(type_choices).forEach(function(type_choice) {
+ type_choice.radio.onclick = function(event) {
+ const radio = event.target;
+ radio.checked = true;
+ radio.setAttribute('aria-expanded', 'true');
+
+ Object.values(type_choices).forEach(function(x) {
+ const isSame = radio.isEqualNode(x.radio);
+ if (isSame) {
+ x.div.classList.remove('d-none');
+ x.div.setAttribute('aria-hidden', 'false');
+ } else {
+ x.div.classList.add('d-none');
+ x.div.setAttribute('aria-hidden', 'true');
+ x.radio.checked = false;
+ x.radio.setAttribute('aria-expanded', 'false');
+ }
+ const inputs = x.div.getElementsByTagName('input');
+ for (let i = 0; i < inputs.length; i++) {
+ inputs[i].required = isSame;
+ }
+ });
+ };
+ });
+
+ const create_select = function(parentNode, select_label, options) {
+ const select = document.createElement('select');
+ select.classList.add('form-select');
+ select.setAttribute('aria-label', select_label);
+ parentNode.appendChild(select);
+ return [
+ select,
+ Object.fromEntries(options.map(function(x) {
+ const option = document.createElement('option');
+ option.appendChild(document.createTextNode(x.text));
+ option.value = x.id;
+ select.appendChild(option);
+ return [x.id, option];
+ }))
+ ];
+ };
+
+ const format_date = function(d, s) { /* YYYY-MM-DD or YYYYMMDD */
+ return d.getFullYear() .toString().padStart(4, '0') + (s ?? '-')
+ + (d.getMonth()+1).toString().padStart(2, '0') + (s ?? '-')
+ + d.getDate() .toString().padStart(2, '0');
+ };
+ const parse_date = (m) => new Date( /* YYYY-MM-DD */
+ parseInt(m.slice(0,4), 10),
+ parseInt(m.slice(5,7), 10)-1,
+ parseInt(m.slice(8,10),10),
+ 12 /* use 12:00:00.0 like for URL parameters */
+ );
+
+ (function(type_choice) {
+ const div = document.createElement('div');
+ div.classList.add('input-group');
+ type_choice.div.appendChild(div);
+
+ type_choice.operator = create_select(div, 'Före eller efter datumet', [
+ { id: '<=', text: 'Tidigast' },
+ { id: '>=', text: 'Senast' },
+ ]);
+
+ const input_quantity = type_choice.quantity = document.createElement('input');
+ input_quantity.classList.add('form-control');
+ input_quantity.type = 'number';
+ input_quantity.min = 0;
+ input_quantity.max = 65535;
+ input_quantity.placeholder = 'Antal';
+ input_quantity.setAttribute('aria-label', input_quantity.placeholder);
+ div.appendChild(input_quantity);
+
+ type_choice.unit = create_select(div, 'Enhet', [
+ { id: 'd', text: 'dagar' },
+ { id: 'w', text: 'veckor' },
+ { id: 'm', text: 'månader' },
+ { id: 'y', text: 'år' },
+ ]);
+
+ const span = document.createElement('span');
+ span.classList.add('input-group-text');
+ div.appendChild(span);
+ span.innerHTML = 'sedan';
+
+ const p = document.createElement('p');
+ p.classList.add('small', 'text-muted', 'invisible');
+ type_choice.div.appendChild(p);
+ p.appendChild(document.createTextNode('Det vill säga '));
+ const span_date = document.createElement('span');
+ p.appendChild(span_date);
+ p.appendChild(document.createTextNode(' eller '));
+ const span_direction = document.createElement('span');
+ p.appendChild(span_direction);
+ p.appendChild(document.createTextNode('.'));
+
+ type_choice._update_helptext = function() {
+ const d = age_filter_settings.get_relative_date(
+ parseInt(type_choice.quantity.value, 10),
+ type_choice.unit[0].value
+ );
+ const operator = type_choice.operator[0].value;
+ if (d == null || operator === '') {
+ p.classList.add('invisible');
+ return null;
+ } else {
+ /* update help text */
+ /* TODO auto update the date passed midnight (if the modal is open) */
+ span_date.innerHTML = format_date(d);
+ span_direction.innerHTML = { '>=':'tidigare', '<=':'senare' }[operator];
+ p.classList.remove('invisible');
+ return d;
+ }
+ };
+
+ type_choice.operator[0].onchange = input_quantity.onchange
+ = type_choice.unit[0].onchange
+ = function(event) {
+ const d = type_choice._update_helptext();
+ if (d != null) {
+ /* propagate to interval tab */
+ const operator = type_choice.operator[0].value;
+ const [d1, d2] = { '>=': [new Date(1900, 0, 1, 12), d], /* between 1900-01-01 and d */
+ '<=': [d, new Date()], /* between d and today */
+ }[operator];
+ type_choices.interval.from.value = format_date(d1);
+ type_choices.interval.to.value = format_date(d2);
+ }
+ };
+ })(type_choices.relative);
+
+ (function(type_choice) {
+ const div = document.createElement('div');
+ div.classList.add('input-group');
+ type_choice.div.appendChild(div);
+
+ const span1 = document.createElement('span');
+ span1.classList.add('input-group-text');
+ span1.innerHTML = 'Mellan';
+ span1.id = 'age-filter-interval-from';
+ div.appendChild(span1);
+
+ const date1 = type_choice.from = document.createElement('input');
+ date1.classList.add('form-control');
+ date1.type = 'date';
+ date1.setAttribute('aria-label', 'Intervallstart');
+ date1.setAttribute('aria-describedby', span1.id);
+ div.appendChild(date1);
+
+ const span2 = document.createElement('span');
+ span2.classList.add('input-group-text');
+ span2.innerHTML = 'och';
+ span2.id = 'age-filter-interval-to';
+ div.appendChild(span2);
+
+ const date2 = type_choice.to = document.createElement('input');
+ date2.classList.add('form-control');
+ date2.type = 'date';
+ date2.setAttribute('aria-label', 'Intervallstop');
+ date2.setAttribute('aria-describedby', span2.id);
+ div.appendChild(date2);
+
+ /* propagate to relative tab by trying to preserve the operator and unit */
+ const propagate_to_relative = function() {
+ let d;
+ switch (type_choices.relative.operator[0].value) {
+ case '<=':
+ d = parse_date(date1.value);
+ break;
+ case '>=':
+ d = parse_date(date2.value);
+ break;
+ }
+ const delta = (new Date().getTime() - d.getTime()) / 86_400_000.;
+ let v;
+ switch (type_choices.relative.unit[0].value) {
+ case 'd':
+ v = delta;
+ break;
+ case 'w':
+ v = delta/7;
+ break;
+ case 'm':
+ v = delta*12/365.25;
+ break;
+ case 'y':
+ v = delta/365.25;
+ break;
+ }
+ if (v != null) {
+ type_choices.relative.quantity.value = Math.round(v);
+ type_choices.relative._update_helptext();
+ }
+ };
+
+ /* make sure that from_date ≤ to_date */
+ date1.onchange = function(event) {
+ if (date1.value !== '' && (date2.value === '' || date1.value > date2.value)) {
+ date2.value = date1.value;
+ }
+ propagate_to_relative();
+ };
+ date2.onchange = function(event) {
+ if (date2.value !== '' && (date1.value === '' || date1.value > date2.value)) {
+ date1.value = date2.value;
+ }
+ propagate_to_relative();
+ };
+ })(type_choices.interval);
+
+ const show_unknown_age = (function() {
+ const div = document.createElement('div');
+ div.classList.add('form-check', 'form-switch');
+ form.appendChild(div);
+
+ const checkbox = document.createElement('input');
+ checkbox.classList.add('form-check-input');
+ checkbox.type = 'checkbox';
+ checkbox.id = 'age-filter-show-unknown';
+ checkbox.setAttribute('role', 'switch');
+ checkbox.checked = age_filter_settings.show_unknown;
+ div.appendChild(checkbox);
+
+ const lbl = document.createElement('label');
+ lbl.classList.add('form-check-label');
+ lbl.setAttribute('for', checkbox.id);
+ lbl.innerHTML = 'Visa även objekt med okänd datum.';
+ div.appendChild(lbl);
+
+ return checkbox;
+ })();
+
+ const footer = document.createElement('div');
+ footer.classList.add('modal-footer');
+ content.appendChild(footer);
+
+ const btn_cancel = document.createElement('button');
+ btn_cancel.classList.add('btn', 'btn-secondary');
+ btn_cancel.type = 'button';
+ btn_cancel.innerHTML = 'Återställ';
+ footer.appendChild(btn_cancel);
+
+ const btn_apply = document.createElement('input');
+ btn_apply.classList.add('btn', 'btn-primary');
+ btn_apply.type = 'submit';
+ btn_apply.value = 'Filter';
+ btn_apply.setAttribute('form', form.id);
+ footer.appendChild(btn_apply);
+
+ btn_cancel.onclick = function(event) {
+ /* deactivate deactivate the filter but preserve its settings */
+ age_filter_settings.active = false;
+ ['avverk', 'mrr', 'vbk'].forEach((l) => mapLayers[l]?.changed());
+
+ const params = new URLSearchParams(window.location.hash.substring(1));
+ params.delete('age-filter');
+ params.delete('show-unknown-age');
+ location.hash = '#' + params.toString();
+
+ modal.hide();
+ };
+
+ form.onsubmit = function(event) {
+ event.preventDefault();
+ const [filter_type, filter_settings] = Object.entries(type_choices).filter(function([id, x]) {
+ return x.radio.checked;
+ })[0];
+ let param;
+ age_filter_settings._min_age = age_filter_settings._max_age = null;
+ switch (filter_type) {
+ case 'relative': {
+ const operator = age_filter_settings.operator = filter_settings.operator[0].value;
+ age_filter_settings.quantity = parseInt(filter_settings.quantity.value, 10);
+ age_filter_settings.unit = filter_settings.unit[0].value;
+ param = {'<=':'-', '>=':''}[operator];
+ param += age_filter_settings.quantity.toString() + age_filter_settings.unit;
+ break;
+ }
+ case 'interval': {
+ const date1 = age_filter_settings.from = parse_date(filter_settings.from.value);
+ const date2 = age_filter_settings.to = parse_date(filter_settings.to.value);
+ param = format_date(date1, '') + '-' + format_date(date2, '');
+ break;
+ }
+ default:
+ return;
+ }
+ age_filter_settings.type = filter_type;
+ age_filter_settings.show_unknown = show_unknown_age.checked;
+ age_filter_settings.setup_minmax();
+ age_filter_settings.active = true;
+ /* TODO auto update the filter passed midnight (if active) */
+ ['avverk', 'mrr', 'vbk'].forEach((l) => mapLayers[l]?.changed());
+
+ const params = new URLSearchParams(window.location.hash.substring(1));
+ params.set('age-filter', param);
+ params.set('show-unknown-age', show_unknown_age.checked ? '1' : '0');
+ location.hash = '#' + params.toString();
+
+ modal.hide();
+ };
+
+ /* Now that all elements have been added to the form, click the radio
+ * button to show the relevant <div>, mark its fields as required and
+ * fills them. The function is run whenever the modal is shown. */
+ return function() {
+ const type_choice = type_choices[age_filter_settings.type];
+ type_choice.radio.click();
+
+ switch (age_filter_settings.type) {
+ case 'relative': {
+ Object.entries(type_choice.operator[1]).map(function([id, option]) {
+ option.selected = id === age_filter_settings.operator;
+ });
+ type_choice.quantity.value = age_filter_settings.quantity.toString();
+ Object.entries(type_choice.unit[1]).map(function([id, option]) {
+ option.selected = id === age_filter_settings.unit;
+ });
+ type_choice.quantity.dispatchEvent(new Event('change')); /* propagate to absolute */
+ break;
+ }
+ case 'interval': {
+ type_choice.from.value = format_date(age_filter_settings.from);
+ type_choice.to.value = format_date(age_filter_settings.to);
+ type_choice.from.dispatchEvent(new Event('change')); /* propagate to relative */
+ break;
+ }
+ }
+ };
+ })();
+
+ const modal = new Modal(panel, {
+ backdrop: false,
+ });
+
+ const backdrop = document.getElementById('age-filter-modal-backdrop');
+ backdrop.onclick = function(event) {
+ modal.hide();
+ };
+
+ const btn = document.getElementById('age-filter-button').getElementsByTagName('button')[0];
+ if (age_filter_settings.active) {
+ btn.classList.replace('btn-light', 'btn-dark');
+ }
+ panel.addEventListener('show.bs.modal', function() {
+ backdrop.classList.add('modal-backdrop', 'show');
+ btn.setAttribute('aria-expanded', 'true');
+ btn.classList.replace('btn-light', 'btn-dark');
+ });
+ panel.addEventListener('hide.bs.modal', function() {
+ /* XXX workaround for https://github.com/twbs/bootstrap/issues/41005#issuecomment-2585390544 */
+ const activeElement = document.activeElement;
+ if (activeElement instanceof HTMLElement) {
+ activeElement.blur();
+ }
+ });
+ panel.addEventListener('hidden.bs.modal', function() {
+ if (!age_filter_settings.active) {
+ btn.classList.replace('btn-dark', 'btn-light');
+ }
+ btn.setAttribute('aria-expanded', 'false');
+ backdrop.classList.remove('modal-backdrop', 'show');
+ });
+
+ btn.onclick = function(event) {
+ dialog_setup();
+ modal.show();
+ };
+})();
+
/* popup and feature overlays */
(function() {
const popupOverlay = new Overlay({