diff options
-rw-r--r-- | index.html | 6 | ||||
-rw-r--r-- | main.js | 695 | ||||
-rw-r--r-- | style.css | 54 |
3 files changed, 681 insertions, 74 deletions
@@ -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>© Guilhem Moulin</p> + <p>© 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>© Guilhem Moulin</p> + <p>© 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> @@ -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); @@ -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; |