aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--index.html6
-rw-r--r--main.js695
-rw-r--r--style.css54
3 files changed, 681 insertions, 74 deletions
diff --git a/index.html b/index.html
index 6fced1d..f4dec03 100644
--- a/index.html
+++ b/index.html
@@ -14,7 +14,7 @@
<div id="map-menu"></div>
</div>
<div id="popup"></div>
- <div class="modal" id="modal-info" tabindex="-1" aria-hidden="true">
+ <div class="modal" id="info-modal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable modal-lg">
<div class="modal-content">
<div class="modal-header">
@@ -31,14 +31,14 @@
</li>
<li class="list-group-item">
<h6>Webbkartan</h6>
- <p>&copy; Guilhem Moulin</p>
+ <p>&copy; 2024-2025 Guilhem Moulin</p>
<p>Licensvillkor: <a href="https://www.gnu.org/licenses/agpl-3.0.en.html" target="_blank">AGPLv3+</a></p>
<p class="small text-muted"><i class="bi bi-file-earmark-code"></i>
<a href="https://git.guilhem.org/KlimatanalysNorr/webmap" target="_blank">Källkod <i class="bi bi-box-arrow-up-right"></i></a></p>
</li>
<li class="list-group-item">
<h6>Backend verktyg</h6>
- <p>&copy; Guilhem Moulin</p>
+ <p>&copy; 2024-2025 Guilhem Moulin</p>
<p>Licensvillkor: <a href="https://www.gnu.org/licenses/gpl-3.0.en.html" target="_blank">GPLv3+</a> och
(endast CGI) <a href="https://www.gnu.org/licenses/agpl-3.0.en.html" target="_blank">AGPLv3+</a></p>
<p class="small text-muted"><i class="bi bi-file-earmark-code"></i>
diff --git a/main.js b/main.js
index 693dd99..52d27da 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_ts: null,
+ _max_ts: null,
+ _date_to_ts: 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_ts = this._max_ts = null;
+ switch (this.type) {
+ case 'relative': {
+ const date = this.get_relative_date(this.quantity, this.unit);
+ const prop = {'<=':'_min_ts', '>=':'_max_ts'}[this.operator];
+ this[prop] = this._date_to_ts(date);
+ break;
+ }
+ case 'interval': {
+ this._min_ts = this._date_to_ts(this.from);
+ this._max_ts = this._date_to_ts(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;
+ }
+ }
})();
@@ -187,11 +290,21 @@ const container = document.getElementById('map-control-container');
const container0 = map.getViewport().getElementsByClassName('ol-overlaycontainer-stopevent')[0];
container0.appendChild(document.getElementById('zoom-control'));
container0.appendChild(container);
- container0.appendChild(document.getElementById('modal-info'));
-
- const backdrop = document.createElement('div');
- container0.appendChild(backdrop);
- backdrop.id = 'modal-info-backdrop';
+ container0.appendChild(document.getElementById('info-modal'));
+
+ 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') {
@@ -350,11 +467,8 @@ if (window.location === window.parent.location) {
control.addEventListener('enterfullscreen', function() {
featureOverlayLayer.setVisible(false);
- const popover = Popover.getInstance(popup);
- if (popover !== null) {
- /* dispose popover as entering fullscreen messes up its position */
- popover.dispose();
- }
+ /* dispose popover as entering fullscreen messes up its position */
+ Popover.getInstance(popup)?.dispose();
const btn = control.element.getElementsByTagName('button')[0];
btn.classList.replace(classInactive, classActive);
@@ -369,11 +483,8 @@ if (window.location === window.parent.location) {
})
control.addEventListener('leavefullscreen', function() {
featureOverlayLayer.setVisible(false);
- const popover = Popover.getInstance(popup);
- if (popover !== null) {
- /* dispose popover as is might overflow the viewport */
- popover.dispose();
- }
+ /* dispose popover as is might overflow the viewport */
+ Popover.getInstance(popup)?.dispose();
const btn = control.element.getElementsByTagName('button')[0];
btn.classList.replace(classActive, classInactive);
@@ -457,12 +568,12 @@ if (window.location === window.parent.location) {
btn.appendChild(i);
i.classList.add('bi', 'bi-info-lg');
- const panel = document.getElementById('modal-info');
+ const panel = document.getElementById('info-modal');
const modal = new Modal(panel, {
backdrop: false,
});
- const backdrop = document.getElementById('modal-info-backdrop');
+ const backdrop = document.getElementById('info-modal-backdrop');
backdrop.onclick = function(event) {
modal.hide();
};
@@ -510,7 +621,7 @@ if (window.location === window.parent.location) {
infoMetadataAccordions.forEach((x) => x.element.replaceChildren());
modal.show();
Promise.allSettled(Object.entries(mapLayers).map(function([grp,lyr]) {
- if (lyr != null && lyr.getSource() instanceof VectorTile) {
+ if (lyr?.getSource() instanceof VectorTile) {
const url = lyr.getSource().getUrls()[0];
if (url == null || url.length <= 16 || url.substr(url.length - 16) !== '/{z}/{x}/{y}.pbf') {
return new Promise(() => { throw new Error(`Invalid URL ${url}`); });
@@ -574,11 +685,11 @@ if (window.location === window.parent.location) {
x.items.forEach(function([groupname, layername]) {
/* for each source file associated with the accordion header, show copyright, license and timing information */
const layer_group = metadata[groupname];
- if (layer_group == null || layer_group.layers == null || layer_group.source_files == null) {
+ if (layer_group?.layers == null || layer_group?.source_files == null) {
return;
}
const def = layer_group.layers[layername];
- if (def == null || def.source_files == null) {
+ if (def?.source_files == null) {
return;
}
def.source_files.forEach(function(source_file) {
@@ -677,10 +788,7 @@ container.setAttribute('aria-hidden', 'false');
view.on('change', function(event) {
featureOverlayLayer.setVisible(false);
- const popover = Popover.getInstance(popup);
- if (popover !== null) {
- popover.dispose();
- }
+ Popover.getInstance(popup)?.dispose();
const coordinates = view.getCenter();
const searchParams = new URLSearchParams(location.hash.substring(1));
@@ -4132,11 +4240,11 @@ const layerHierarchy = [
text: 'Skogsbruk',
children: [
{
- text: 'Uppförda (sedan 2000)',
+ text: 'Utförda avverkningar',
layer: 'avverk.utford',
},
{
- text: 'Anmälda',
+ text: 'Avverkningsanmälningar',
layer: 'avverk.anmald',
},
]
@@ -4393,6 +4501,7 @@ const [mapLayers, featureOverlayLayer] = (function() {
* misc at the end so they show up on top of suface features */
const rasterLayers = ['kskog'];
const vectorLayers = ['nv', 'mrr', 'skydd', 'ren', 'ri', 'avverk', 'vbk', 'svk', 'misc'];
+ const canFilterByAge = ['avverk', 'mrr', 'vbk']; /* layers for which features are dated */
const ret = {};
if (!canWebGL2) {
@@ -4420,6 +4529,7 @@ const [mapLayers, featureOverlayLayer] = (function() {
}
vectorLayers.forEach(function(k) {
+ const canFilterByAge0 = canFilterByAge.includes(k);
ret[k] = new VectorTileLayer({
source: new VectorTile({
url: baseurl + 'tiles/' + k + xyz,
@@ -4435,7 +4545,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 ts = properties.ts;
+ if (ts == null) {
+ if (canFilterByAge0 && !age_filter_settings.show_unknown) {
+ return null;
+ }
+ } else if ((age_filter_settings._min_ts !== null && ts < age_filter_settings._min_ts) ||
+ (age_filter_settings._max_ts !== null && ts > age_filter_settings._max_ts)) {
+ return null;
+ }
+ }
+ const style = styles[k + '.' + properties.layer];
if (!Array.isArray(style)) {
return style;
} else {
@@ -4449,6 +4573,7 @@ const [mapLayers, featureOverlayLayer] = (function() {
}
});
ret[k].set('layerGroup', k, true);
+ ret[k].set('canFilterByAge', canFilterByAge0, true);
map.addLayer(ret[k]);
});
@@ -4686,11 +4811,8 @@ const infoMetadataAccordions = [];
}
Object.entries(result).forEach(function([lyr, visible]) {
- const v = mapLayers[lyr];
- if (v != null) {
- //console.log(lyr, visible);
- v.setVisible(visible);
- }
+ //console.log(lyr, visible);
+ mapLayers[lyr]?.setVisible(visible);
});
const btn = document.getElementById('map-legend-button');
if (Object.values(styles).some((v) => v !== null)) {
@@ -4702,17 +4824,14 @@ const infoMetadataAccordions = [];
const onClickFunction = function(layerList, event) {
featureOverlayLayer.setVisible(false);
featureOverlayLayer.changed();
- const popover = Popover.getInstance(popup);
- if (popover !== null) {
- popover.dispose();
- }
+ Popover.getInstance(popup)?.dispose();
+
const searchParams = new URLSearchParams(location.hash.substring(1));
let layersParams = searchParams.get('layers') || '';
layersParams = layersParams.match(/^\s*$/) ? [] : layersParams.split(' ');
if (event.target.checked) {
layerList.forEach(function(lyr) {
- const l = mapLayers[lyr.split('.', 1)[0]];
- if (l == null) {
+ if (mapLayers[lyr.split('.', 1)[0]] == null) {
return; /* keep unexisting layers (eg WebGL layers on a system without WebGL support) unselectable */
}
styles[lyr] = layers[lyr].style;
@@ -4920,6 +5039,478 @@ 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 t.ex. filtrera bort gamla objekt. ' +
+ '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 type_choices = Object.fromEntries([
+ { id: 'relative', text: 'Åldersgräns', desc: 'som är' },
+ { id: 'interval', text: 'Datumintervall', desc: 'med datum' },
+ ].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);
+
+ const p = document.createElement('p');
+ p.classList.add('age-filter-settings-desc');
+ div.appendChild(p);
+ const t = document.createTextNode('Visa endast objekt ' + x.desc + ':');
+ p.appendChild(t);
+
+ 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, 'Under eller över åldersgränsen', [
+ { id: '<=', text: 'Nyare' },
+ { id: '>=', text: 'Äldre' },
+ ]);
+
+ const span = document.createElement('span');
+ span.classList.add('input-group-text');
+ div.appendChild(span);
+ span.innerHTML = 'än';
+
+ 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 p = document.createElement('p');
+ p.classList.add('small', 'text-muted', 'invisible');
+ type_choice.div.appendChild(p);
+ p.appendChild(document.createTextNode('Det vill säga med datum '));
+ 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;
+ Object.values(mapLayers).forEach(function(lyr) {
+ if (lyr?.get('canFilterByAge')) {
+ lyr.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_ts = age_filter_settings._max_ts = 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) */
+ Object.values(mapLayers).forEach(function(lyr) {
+ if (lyr?.get('canFilterByAge')) {
+ lyr.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({
@@ -5029,7 +5620,7 @@ const infoMetadataAccordions = [];
};
const onClickPageChange = function(event, offset) {
const btn = event.target;
- if (btn.classList.contains('disabled') || popover === null || popover.tip === null) {
+ if (btn.classList.contains('disabled') || popover?.tip == null) {
return;
}
if (overlayAttrIdx + offset < 0 || overlayAttrIdx + offset > overlayAttributes.length - 1) {
@@ -5077,7 +5668,7 @@ const infoMetadataAccordions = [];
const btnExpandTitle2 = 'Förminska';
btnExpand.setAttribute('aria-label', btnExpand.title);
btnExpand.onclick = function(event) {
- if (popover === null || popover.tip === null) {
+ if (popover?.tip == null) {
return;
}
if (!popover.tip.classList.contains('popover-maximized')) {
@@ -5102,9 +5693,7 @@ const infoMetadataAccordions = [];
btnClose.onclick = function(event) {
featureOverlayLayer.setVisible(false);
featureOverlayLayer.changed();
- if (popover !== null) {
- popover.dispose();
- }
+ popover?.dispose();
};
header.appendChild(btnPrev);
@@ -5220,7 +5809,7 @@ const infoMetadataAccordions = [];
btnNext.classList.add('d-none', 'disabled');
/* never start in maximized mode */
- if (popover !== null && popover.tip !== null) {
+ if (popover?.tip != null) {
popover.tip.classList.remove('popover-maximized');
}
btnExpand.classList.replace('popover-button-reduce', 'popover-button-expand');
@@ -5232,11 +5821,11 @@ const infoMetadataAccordions = [];
const layerGroup = layer.get('layerGroup');
const layerName = feature.getProperties().layer;
const def = layers[layerGroup + '.' + layerName];
- if (def !== undefined && def.popover !== undefined) {
+ if (def?.popover != null) {
/* skip layers which didn't opt-in for popover */
if (!fetch_body.length) {
document.body.classList.add('inprogress');
- if (popover !== null && popover.tip !== null) {
+ if (popover?.tip != null) {
popover.tip.classList.add('inprogress');
}
}
@@ -5256,10 +5845,8 @@ const infoMetadataAccordions = [];
});
if (fetch_body.length === 0) {
- if (popover !== null) {
- /* dispose pre-detached popover */
- popover.dispose();
- }
+ /* dispose pre-detached popover */
+ popover?.dispose();
return;
}
@@ -5283,10 +5870,8 @@ const infoMetadataAccordions = [];
* decoded JSON response would need to be reordered to match fetch_body */
overlayAttributes = data
if (overlayAttributes.length === 0) {
- if (popover !== null) {
- /* dispose pre-detached popover */
- popover.dispose();
- }
+ /* dispose pre-detached popover */
+ popover?.dispose();
return;
}
@@ -5297,7 +5882,7 @@ const infoMetadataAccordions = [];
btnPrev.classList.remove('d-none');
pageNode.classList.remove('d-none');
}
- if (popover === null || popover.tip === null) {
+ if (popover?.tip == null) {
/* create a new popover (we're not already showing one in detached mode) */
pageNum.innerHTML = (overlayAttrIdx + 1).toString();
popupOverlay.setPosition(event.coordinate);
diff --git a/style.css b/style.css
index f979bd0..672610d 100644
--- a/style.css
+++ b/style.css
@@ -267,6 +267,7 @@ body.inprogress {
@media screen and (max-width: 200px) {
#layer-selection-button,
#map-legend-button,
+ #age-filter-button,
#export-to-image,
#layer-selection-panel,
#map-legend-panel,
@@ -288,7 +289,7 @@ body.inprogress {
--bs-btn-padding-y: 0.4rem;
}
}
-#modal-info {
+#info-modal {
/* close the modal when clicking the backdrop */
pointer-events: none;
-webkit-user-select: text;
@@ -299,11 +300,11 @@ body.inprogress {
--modal-info-padding-y: .5rem;
--modal-info-bg-light: rgba(0, 0, 0, .08);
}
-#modal-info .modal-header {
+#info-modal .modal-header {
padding: var(--modal-info-padding-y) var(--modal-info-padding-x);
}
-#modal-info .list-group-item,
-#modal-info {
+#info-modal .list-group-item,
+#info-modal {
--bs-list-group-border-width: 1px;
}
#info-accordion {
@@ -317,20 +318,20 @@ body.inprogress {
--bs-accordion-btn-active-bg: var(--modal-info-bg-light);
padding: 0 var(--modal-info-padding-x);
}
-#modal-info .accordion-item {
+#info-modal .accordion-item {
border: none;
}
-#modal-info .accordion-header > .accordion-button[aria-expanded="false"]:hover {
+#info-modal .accordion-header > .accordion-button[aria-expanded="false"]:hover {
background-color: rgb(from var(--modal-info-bg-light) r g b / calc(alpha*.4));
}
-#modal-info .accordion-header > .accordion-button[aria-expanded="true"] {
+#info-modal .accordion-header > .accordion-button[aria-expanded="true"] {
background-color: var(--bs-accordion-btn-active-bg);
}
-#modal-info ul.list-group > li.list-group-item {
+#info-modal ul.list-group > li.list-group-item {
padding: .3rem 0;
border: none;
}
-#modal-info ul.list-group > li.list-group-item:last-child {
+#info-modal ul.list-group > li.list-group-item:last-child {
padding-bottom: var(--modal-info-padding-y);
}
#info-body > ul.list-group {
@@ -338,36 +339,36 @@ body.inprogress {
border-top: var(--bs-list-group-border-width) solid var(--modal-info-bg-light);
padding: 0 var(--modal-info-padding-x);
}
-#modal-info ul.list-group > li.list-group-item:not(:first-child) {
+#info-modal ul.list-group > li.list-group-item:not(:first-child) {
border-top: var(--bs-list-group-border-width) solid var(--modal-info-bg-light);
}
#info-accordion > .accordion-item:not(:last-child) ul.list-group > li.list-group-item:last-child {
border-bottom: var(--bs-list-group-border-width) solid var(--modal-info-bg-light);
}
-#modal-info .modal-body ul.list-group > li.list-group-item:not(.text-muted):hover {
+#info-modal .modal-body ul.list-group > li.list-group-item:not(.text-muted):hover {
background-color: rgb(from var(--modal-info-bg-light) r g b / calc(alpha*.4));
}
#info-body {
padding: var(--modal-info-padding-y) 0;
}
#info-body > ul.list-group > li.list-group-item p,
-#modal-info .accordion-body ul.list-group > li.list-group-item p {
+#info-modal .accordion-body ul.list-group > li.list-group-item p {
margin: 0;
}
#info-body > ul.list-group > li.list-group-item h6,
-#modal-info .accordion-body ul.list-group > li.list-group-item h6 {
+#info-modal .accordion-body ul.list-group > li.list-group-item h6 {
margin: .05rem 0 .15rem 0;
font-size: 1.15rem;
}
-#modal-info .modal-body a {
+#info-modal .modal-body a {
color: inherit;
text-decoration: none;
}
-#modal-info .modal-body a:hover {
+#info-modal .modal-body a:hover {
opacity: .8;
text-decoration: underline;
}
-#modal-info .modal-body .info-credits {
+#info-modal .modal-body .info-credits {
border-top: var(--bs-list-group-border-width) solid var(--modal-info-bg-light);
padding: var(--modal-info-padding-y) var(--modal-info-padding-x) 0 var(--modal-info-padding-x);
margin: 0;
@@ -463,6 +464,27 @@ body.inprogress {
background-color: var(--bs-list-group-action-hover-bg);
}
+#age-filter-modal .modal-body .btn-group {
+ width: 100%;
+}
+#age-filter-modal .age-filter-infotext {
+ margin: 0 0 var(--bs-modal-padding) 0;
+}
+#age-filter-form .age-filter-settings .age-filter-settings-desc {
+ margin: var(--bs-modal-padding) 0;
+}
+#age-filter-form .age-filter-settings {
+ margin: 0 0 var(--bs-modal-padding) 0;
+}
+#age-filter-form .age-filter-settings p.text-muted {
+ margin: 0;
+}
+#age-filter-modal p {
+ -webkit-user-select: text;
+ -moz-user-select: text;
+ user-select: text;
+}
+
.popover {
--bs-popover-header-padding-x: .75rem;
--bs-popover-header-padding-y: .5rem;