aboutsummaryrefslogtreecommitdiffstats
path: root/main.js
diff options
context:
space:
mode:
authorGuilhem Moulin <guilhem@fripost.org>2025-06-08 17:28:28 +0200
committerGuilhem Moulin <guilhem@fripost.org>2025-06-11 12:41:44 +0200
commit78b07067b4aba79fa0b5f282a067856f41e9ab31 (patch)
tree32e8184ca6d3fca7d766814645470669199fe4d4 /main.js
parente78dc2e27b61fc60c4f378c461196e7cae15ec24 (diff)
Add ability to filter over feature “age”.
Right now this is only implemented for avverk, mrr and vbk as other layers don't export the property.
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({