diff --git a/CHANGELOG.md b/CHANGELOG.md index 879186b0..ebacc3d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project tries to adhere to [Semantic Versioning](https://semver.org/spe - results layer with customizable result views ### Changed +- update test API JSON and dependent JS implementation to reflect changes made in #77 - make data in legend dynamically and add legend schema and example - extend popup schema with key values component - load satellite layer beneath symbol layers #43 diff --git a/digiplan/map/layers.py b/digiplan/map/layers.py index 4c84bcaa..94f9bd8a 100644 --- a/digiplan/map/layers.py +++ b/digiplan/map/layers.py @@ -73,7 +73,8 @@ class RasterLayerData: name="Ergebnisse", name_singular="Ergebnis", description="", - popup_fields=["name"], + popup_fields=("title", "municipality", "key-values", "chart", "description", "sources"), + # order matters ) ] } diff --git a/digiplan/static/js/helper.js b/digiplan/static/js/helper.js index 7e2b5420..f915799b 100644 --- a/digiplan/static/js/helper.js +++ b/digiplan/static/js/helper.js @@ -14,6 +14,13 @@ function logMessage(msg) { } } +function getLanguage() { + // TODO: implement dynamic language determination + // In the future this will properly be implement. + // For now, this function always returns "en" + return "en-US"; +} + // TODO: there is another unmerged branch with this util function (popup.js). // Remove duplicate entry in popup.js after merge. async function fetchGetJson(url) { diff --git a/digiplan/static/js/popup.js b/digiplan/static/js/popup.js index 4138b8eb..05f9ef20 100644 --- a/digiplan/static/js/popup.js +++ b/digiplan/static/js/popup.js @@ -1,33 +1,207 @@ "use strict"; -function add_popup(layer_id, fields, template_id="default") { - map.on("click", layer_id, function (e) { - let coordinates; - // Check if popup already exists: - if ($('.mapboxgl-popup').length > 0) {return;} - - if ("lat" in e.features[0].properties) { - // Get coordinates from lat/lon: - coordinates = [e.features[0].properties.lat, e.features[0].properties.lon]; - } else { - // Get coordinates from geometry: - coordinates = e.lngLat; +async function fetchGetJson(url) { + try { + // Default options are marked with * + const response = await fetch(url, { + method: "GET", // *GET, POST, PUT, DELETE, etc. + mode: "cors", // no-cors, *cors, same-origin + cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached + credentials: "same-origin", // include, *same-origin, omit + headers: { + "Content-Type": "application/json", + }, redirect: "follow", // manual, *follow, error + referrerPolicy: "no-referrer", // no-referrer, *client + }); + return await response.json(); // parses JSON response into native JavaScript objects + } catch (err) { + if (err instanceof Error) { + throw new Error(err.message); } - var template = $($("#" + template_id + "_popup").prop("content")).find("div"); - var clone = template.clone(); - var table = clone.find("table"); - for (var label in fields) { - const key = fields[label]; - const value = e.features[0].properties[key]; - let row = $(""); - row.append($("").text(label)); - row.append($("").text(value)); - table.append(row); + throw err; + } +} + +function createCoordinates(event) { + if ("lat" in event.features[0].properties) { + return [event.features[0].properties.lat, event.features[0].properties.lon]; + } + return event.lngLat; +} + +function createListByName(name, series) { + let list = []; + for (const item in series) { + list.push(series[item][name]); + } + return list; +} + +function add_popup(layer_id, fields, template_id = "default") { + map.on("click", layer_id, function (event) { + /* + Check if popup already exists + */ + if ($('.mapboxgl-popup').length > 0) { + return; } - new maplibregl.Popup().setLngLat(coordinates).setHTML(clone.html()).addTo(map); + + /* + Construct Coordinates From Event + */ + const coordinates = createCoordinates(event); + + // TODO: construct dynamically via emitted id by event + const url = "/static/tests/api/popup.json??lookup=population&municipality=12lang=en"; + + fetchGetJson(url).then(// TODO: for now we assume response has chart. Later determine dynamically. + (response) => { + /* + Construct Popup From Event And Params + */ + const template = document.getElementById("js-" + template_id + "_popup"); + const clone = template.content.cloneNode(true); + const html = clone.getElementById("js-" + "popup"); + for (const field in fields) { + if (field === "title") { + const titleElement = html.querySelector("#js-popup__title"); + const {title} = response; + titleElement.innerHTML = title; + } + if (field === "municipality") { + const municipalityElement = html.querySelector("#js-popup__municipality"); + const {municipality} = response; + municipalityElement.innerHTML = `(${municipality})`; + } + if (field === "key-values") { + const lang = getLanguage(); + const keyValuesElement = html.querySelector("#js-popup__key-values"); + const { + keyValues: { + unit, + year, + municipalityValue, + regionTitle, + regionValue, + } + } = response; + keyValuesElement.innerHTML = ` + + ${unit} + ${year} + ${municipalityValue.toLocaleString(lang)} + + + ${regionTitle}: + ${regionValue.toLocaleString(lang)} + + `; + } + if (field === "description") { + const descriptionElement = html.querySelector("#js-popup__description"); + const {description} = response; + descriptionElement.innerHTML = description; + } + if (field === "chart") { + + // Chart Title + const {chart: {title}} = response; + + // Chart + const chartElement = html.querySelector("#js-popup__chart"); + const chart = echarts.init(chartElement, null, {renderer: 'svg'}); + // TODO: use chartType in payload to construct chart dynamically. For now we assume bar chart type. + // TODO: In this fetch we always expect one payload item. Make failsafe. + const {chart: {data: {series}}} = response; + const xAxisData = createListByName("key", series); + const yAxisData = createListByName("value", series); + const option = { + title: { + text: title, + textStyle: { + color: '#002E50', + fontSize: 14, + fontWeight: 400, + lineHeight: 16 + }, + left: 'center' + }, + animation: false, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'shadow' + } + }, + grid: { + left: 16, + right: 0, + bottom: 32, + top: 48, + containLabel: true + }, + textStyle: { + color: '#002E50' + }, + xAxis: [{ + type: 'category', + data: xAxisData, + axisTick: { + show: false + }, + axisLine: { + show: true, + lineStyle: { + color: '#ECF2F6' + } + }, + }], + yAxis: [{ + type: 'value', + splitLine: { + show: true, + lineStyle: { + color: '#ECF2F6' + } + } + }], + series: [{ + name: 'Direct', + type: 'line', + symbol: 'circle', + symbolSize: 6, + data: yAxisData, + lineStyle: { + color: '#002E50' + }, + itemStyle: { + color: '#002E50' + } + }] + }; + chart.setOption(option); + requestAnimationFrame(() => { + new maplibregl.Popup({ + // https://maplibre.org/maplibre-gl-js-docs/api/markers/#popup-parameters + maxWidth: "280px", + }).setLngLat(coordinates).setHTML(html.innerHTML).addTo(map); + }); + } + if (field === "sources") { + const sourcesElement = html.querySelector("#js-popup__sources"); + let links = []; + for (const index in response.sources) { + const url = response.sources[index].url; + const name = response.sources[index].name; + links.push(`${name}`); + } + sourcesElement.innerHTML = `Quellen: ${links.join(", ")}`; + } + } + }); }); } -$(document).ready(function() { +$(document).ready(function () { $('#js-intro-modal').modal('show'); }); diff --git a/digiplan/static/scss/app.scss b/digiplan/static/scss/app.scss index 4c1668c7..ba8c5e79 100644 --- a/digiplan/static/scss/app.scss +++ b/digiplan/static/scss/app.scss @@ -14,6 +14,7 @@ @import 'components/map_basemaps_control'; @import 'components/modals'; @import 'components/offcanvas'; +@import 'components/popup'; @import 'components/tabs'; @import 'components/top-nav'; diff --git a/digiplan/static/scss/base/_mixins.scss b/digiplan/static/scss/base/_mixins.scss index 19f82401..e7aad3b9 100644 --- a/digiplan/static/scss/base/_mixins.scss +++ b/digiplan/static/scss/base/_mixins.scss @@ -69,6 +69,7 @@ -moz-user-select: none; -ms-user-select: none; user-select: none; + pointer-events: none; } // UL diff --git a/digiplan/static/scss/base/_variables.scss b/digiplan/static/scss/base/_variables.scss index 58833cab..72359ed1 100644 --- a/digiplan/static/scss/base/_variables.scss +++ b/digiplan/static/scss/base/_variables.scss @@ -21,6 +21,7 @@ $font-size-xsmall: .75rem; // 12px $letter-spacing-small: 0.01rem; $letter-spacing-normal: 0.03rem; +$letter-spacing-large: 0.06rem; // LEFT PANEL $panel-width-sm: 22rem; @@ -63,5 +64,8 @@ $map-control-position-left: 1rem; $map-control-z-index: 9; $map-control-bg-color: rgba(255, 255, 255, 1); +// POPUP +$popup-width: 28rem; + // ICONS $info-icon-size: 1.25rem; diff --git a/digiplan/static/scss/components/_popup.scss b/digiplan/static/scss/components/_popup.scss new file mode 100644 index 00000000..7b215093 --- /dev/null +++ b/digiplan/static/scss/components/_popup.scss @@ -0,0 +1,94 @@ +.mapboxgl-popup-content, .maplibregl-popup-content { + @extend .p-0; + width: $popup-width; + font-family: $font-family-sans-serif; + + .popup { + &__header { + @extend .bg-light; + @extend .py-2; + @include flex-row-justify-center; + @include user-select-none; + } + + &__title, + &__municipality { + @extend .fs-4; + @extend .fw-light; + @extend .mb-0; + letter-spacing: $letter-spacing-large; + } + + &__municipality { + @extend .ps-1; + } + + &__key-values, + &__chart, + &__description, + &__sources { + padding: $padding-normal; + } + + &__key-values { + @extend .d-flex; + @extend .flex-row; + @extend .justify-content-between; + @extend .fs-7; + @extend .border-bottom; + @include user-select-none; + + .key-values { + & > span { + @extend .d-block; + } + + &__municipality { + @extend .fw-bold; + } + + &__region { + color: $gray-600; + } + + &__region-value { + @extend .bg-primary; + @extend .text-white; + margin-left: 4px; + } + + &__municipality-value { + @extend .bg-light; + } + + &__region-value, + &__municipality-value { + padding: 4px 8px; + @extend .rounded; + } + } + } + + &__description { + @extend .fw-light; + @extend .fs-7; + } + + &__sources { + @extend .border-top; + @extend .lh-sm; + + a { + @extend .fw-bold; + } + } + } + + .maplibregl-popup-close-button, + .mapboxgl-popup-close-button { + @extend .fs-3; + @extend .fw-light; + @extend .px-2; + @extend .py-1; + } +} diff --git a/digiplan/static/tests/api/popup.json b/digiplan/static/tests/api/popup.json new file mode 100644 index 00000000..9d554d7f --- /dev/null +++ b/digiplan/static/tests/api/popup.json @@ -0,0 +1,59 @@ +{ + "chart": { + "chartType": "bar", + "data": { + "definition": { + "key": { + "lookup": "year", + "name": "Year", + "type": "integer" + }, + "value": { + "lookup": "population", + "name": "Population", + "type": "integer" + } + }, + "series": [ + { + "key": 2022, + "value": 9311 + }, + { + "key": 2030, + "value": 9182 + }, + { + "key": 2045, + "value": 8903 + } + ] + }, + "title": "Population Forecast" + }, + "description": "The population in 2022 of Z\u00f6rbig was 9,311 inhabitants. The entire ABW region had 370,190 inhabitants at that time. The following chart shows a forecast of the population development for the years 2022, 2030, and 2045.", + "id": 12, + "keyValues": { + "municipalityValue": 9311, + "regionTitle": "ABW region", + "regionValue": 370190, + "unit": "Population", + "year": 2022 + }, + "municipality": "Z\u00f6rbig", + "sources": [ + { + "name": "Bev\u00f6lkerungz\u00e4hlung Anhalt 2021", + "url": "https://wam.rl-institut.de/digiplan/sources#11" + }, + { + "name": "Bev\u00f6lkerungsprognose Anhalt 2030", + "url": "https://wam.rl-institut.de/digiplan/sources#12" + }, + { + "name": "Bev\u00f6lkerungsprognose Anhalt 2050", + "url": "https://wam.rl-institut.de/digiplan/sources#13" + } + ], + "title": "Population" +} diff --git a/digiplan/templates/map.html b/digiplan/templates/map.html index 2c2702dc..696acaba 100644 --- a/digiplan/templates/map.html +++ b/digiplan/templates/map.html @@ -9,10 +9,10 @@ {% endblock %} {% block css %} - {{ block.super }} {% compress css %} {% endcompress %} + {{ block.super }} {% endblock %} {% block content %} @@ -358,7 +358,7 @@ {% if debug %} {% for layer in all_layers %} map.on("click", "{{layer.id}}", function (e) { - console.log(e.features[0].properties.name, e.features[0]); + console.log("{{layer.id}}:", e.features[0].properties.name, e.features[0]); }); {% endfor %} setDebugMode(true); diff --git a/digiplan/templates/popups/default.html b/digiplan/templates/popups/default.html index 60531ec4..721d5758 100644 --- a/digiplan/templates/popups/default.html +++ b/digiplan/templates/popups/default.html @@ -1,9 +1,7 @@ - -