diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml
index 77aca2bac..b82bec9ff 100644
--- a/.github/workflows/unit_tests.yml
+++ b/.github/workflows/unit_tests.yml
@@ -11,17 +11,11 @@ jobs:
name: Build & Run Tests
runs-on: ubuntu-latest
- strategy:
- matrix:
- node-version: [18.x]
-
steps:
- - uses: actions/checkout@v2
-
- - name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v2
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v3
with:
- node-version: ${{ matrix.node-version }}
+ node-version: '18'
- name: Install Dependencies
run: npm install
diff --git a/api/api.js b/api/api.js
index 5787ffbc6..d25f1925b 100644
--- a/api/api.js
+++ b/api/api.js
@@ -110,13 +110,6 @@ module.exports = async (req, res) => {
// Assign from _template if provided as path param.
req.params.template ??= req.params._template
- // Decode string params.
- Object.entries(req.params)
- .filter(entry => typeof entry[1] === 'string')
- .forEach(entry => {
- req.params[entry[0]] = decodeURIComponent(entry[1])
- })
-
// Short circuit login view or post request.
if (req.params.login || req.body && req.body.login) return login(req, res)
diff --git a/lib/layer/decorate.mjs b/lib/layer/decorate.mjs
index a2530a8ab..354fe8274 100644
--- a/lib/layer/decorate.mjs
+++ b/lib/layer/decorate.mjs
@@ -14,6 +14,7 @@ export default async layer => {
zoomToExtent,
});
+ // Warn if outdated layer.edit configuration is used.
if (layer.edit) {
console.warn(`Layer: ${layer.key}, please update edit:{} to use draw:{} as layer.edit has been superseeded with layer.draw to be in line with the OL drawing interaction.`)
layer.draw = Object.assign(layer.draw || {}, layer.edit)
@@ -22,6 +23,7 @@ export default async layer => {
// Layer must have an empty draw config to allow for role based assignment of drawing methods.
layer.draw ??= {}
+ // Warn if outdated layer.draw.delete configuration is used.
if (layer.draw?.delete) {
console.warn(`Layer: ${layer.key}, please move draw.delete to use layer.deleteLocation:true.`)
}
@@ -29,8 +31,10 @@ export default async layer => {
// Callback which creates and stores a location from a feature returned by the drawing interaction.
layer.draw.callback = async (feature, params) => {
+ // If the feature is null, return.
if (!feature) return;
+ // Get the current table and set new to true.
const location = {
layer,
table: layer.tableCurrent(),
@@ -56,7 +60,8 @@ export default async layer => {
layer.draw?.defaults || {}))
})
- //layer.reload()
+ // Layer must be reloaded to reflect geometry changes.
+ layer.reload()
// Get the newly created location.
mapp.location.get(location)
@@ -70,6 +75,20 @@ export default async layer => {
// Set layer opacity from style.
layer.L.setOpacity(layer.style?.opacity || 1);
+ // Layer style has multiple hovers and a single hover (incorrect configuration)
+ if (layer.style?.hovers && layer.style?.hover) {
+
+ console.warn(`Layer: ${layer.key}, cannot use both layer.style.hover and layer.style.hovers. Layer.style.hover has been deleted.`)
+ delete layer.style.hover;
+ }
+
+ // Layer style has multiple labels and a single label (incorrect configuration)
+ if (layer.style?.labels && layer.style?.label) {
+
+ console.warn(`Layer: ${layer.key}, cannot use both layer.style.label and layer.style.labels. Layer.style.label has been deleted.`)
+ delete layer.style.label;
+ }
+
// Layer style has multiple themes.
if (layer.style?.themes) {
@@ -80,16 +99,15 @@ export default async layer => {
: layer.style.themes[layer.style.theme || Object.keys(layer.style.themes)[0]];
}
+ // If setLabel is included and labels object exists.
if (layer.style?.theme?.setLabel && layer.style?.labels) {
+ // Swap the label based on the setLabel key.
layer.style.label = layer.style.labels[layer.style.theme.setLabel]
}
- if (layer.style?.theme?.setHover && layer.style?.hovers) {
-
- layer.style.hover = layer.style.hovers[layer.style.theme.setHover]
- }
-
+ // Warn if outdated layer.hover configuration is used.
+ // Set layer.style.hover and remove layer.hover.
if (layer.hover) {
console.warn(`Layer: ${layer.key}, layer.hover{} should be defined within layer.style{}.`)
@@ -97,7 +115,15 @@ export default async layer => {
delete layer.hover;
}
- // Layer style has multiple themes.
+ // If setHover is included and hovers object exists.
+ if (layer.style?.theme?.setHover && layer.style?.hovers) {
+
+ // Swap the hover based on the setHover key.
+ layer.style.hover = layer.style.hovers[layer.style.theme.setHover]
+ }
+
+
+ // Layer style has multiple hovers
if (layer.style?.hovers) {
// Keep object hover.
@@ -113,7 +139,7 @@ export default async layer => {
layer.style.hover.method ??= mapp.layer.featureHover;
}
- // Layer style has multiple themes.
+ // Layer style has multiple labels.
if (layer.style?.labels) {
// Keep object label.
@@ -131,7 +157,7 @@ export default async layer => {
// Check if the role is an object and not null
if (layer.roles[role] !== null && typeof layer.roles[role] === 'object') {
-
+
// Extract the role name from negated roles (e.g., "!role" becomes "role")
const negatedRole = role.match(/(?<=^!)(.*)/g)?.[0];
@@ -191,13 +217,12 @@ export default async layer => {
}
function show() {
- /**
- * Reveals the layer
- */
+ // Show the layer
this.display = true;
- try { // Add layer to map
+ try {
+ // Add layer to map
this.mapview.Map.addLayer(this.L);
} catch {
// Will catch assertation error when layer is already added.
@@ -217,10 +242,8 @@ function show() {
}
function hide() {
- /**
- * Hides the layer
- */
+ // Hide the layer.
this.display = false;
// Remove OL layer from mapview.
@@ -237,9 +260,8 @@ function hide() {
}
function tableCurrent() {
- /**
- * Returns the current table
- */
+
+ // Return the current table if it exists.
// A layer must have either a table or tables configuration.
if (!this.tables) return this.table;
@@ -269,9 +291,8 @@ function tableCurrent() {
}
function tableMax() {
- /**
- * Returns the max table
- */
+
+ // Returns the max table
// A layer must have either a table or tables configuration.
if (!this.tables) return this.table;
@@ -281,9 +302,8 @@ function tableMax() {
}
function tableMin() {
- /**
- * Returns the min table
- */
+
+ // Returns the min table.
// A layer must have either a table or tables configuration.
if (!this.tables) return this.table;
@@ -293,9 +313,8 @@ function tableMin() {
}
async function zoomToExtent(params) {
- /**
- * Zooms to a specific extent
- */
+
+ // Zooms to a specific extent
// XMLHttpRequest to layer extent endpoint
let response = await mapp.utils.xhr(`${this.mapview.host}/api/query/layer_extent?` +
diff --git a/lib/layer/featureHover.mjs b/lib/layer/featureHover.mjs
index 4f52a7d94..df8e2b071 100644
--- a/lib/layer/featureHover.mjs
+++ b/lib/layer/featureHover.mjs
@@ -3,12 +3,12 @@ export default function (feature, layer) {
// The hover method must only execute if the display flag is set.
if (!layer.style.hover.display) return;
- if (!layer.mapview.interaction.current) return;
+ // Store current highlight (feature) key.
+ const featureKey = layer.mapview.interaction?.current?.key?.toString()
- // Store current highlight key.
- let key = layer.mapview.interaction.current.key.toString()
+ if (!featureKey) return;
- let paramString = mapp.utils.paramString({
+ const paramString = mapp.utils.paramString({
dbs: layer.dbs,
locale: layer.mapview.locale.key,
layer: layer.key,
@@ -29,20 +29,23 @@ export default function (feature, layer) {
mapp.utils.xhr(`${layer.mapview.host}/api/query?${paramString}`)
.then(response => {
- // Check whether highlight feature is still current.
- if (layer.mapview.interaction?.current?.key !== key) return;
-
- // Check whether cursor has position (in map).
- if (!layer.mapview.position) return;
-
// Check whether there is a response to display.
if (!response) return;
+ if (typeof layer.style.hover.render === 'function') {
+
+ const content = layer.style.hover.render(response)
+ layer.mapview.infotip(content)
+ return;
+ }
+
// Check whether the response label field has a value.
- if (response.label == '') return;
+ if (response.label == '') return;
+
+ // Check whether highlight feature is still current.
+ if (layer.mapview.interaction?.current?.key !== featureKey) return;
// Display the response label as infotip.
layer.mapview.infotip(response.label)
})
-
}
\ No newline at end of file
diff --git a/lib/layer/format/_format.mjs b/lib/layer/format/_format.mjs
index 628e37a5f..c9988c163 100644
--- a/lib/layer/format/_format.mjs
+++ b/lib/layer/format/_format.mjs
@@ -15,5 +15,6 @@ export default {
mvt,
cluster: vector,
geojson: vector,
- wkt: vector
+ wkt: vector,
+ vector
}
\ No newline at end of file
diff --git a/lib/layer/format/vector.mjs b/lib/layer/format/vector.mjs
index 76394e2d4..9f91834a8 100644
--- a/lib/layer/format/vector.mjs
+++ b/lib/layer/format/vector.mjs
@@ -1,35 +1,19 @@
export default layer => {
- if (!layer.srid) {
- console.warn(`No SRID provided for ${layer.key}`)
- }
+ // 3857 is assumed to be the default SRID for all vector format layer.
+ layer.srid ??= '3857'
if (layer.properties) {
- console.warn(`Layer: ${layer.key},layer.properties{} are no longer required for wkt & geojson datasets.`)
+ console.warn(`Layer: ${layer.key}, layer.properties{} are no longer required for wkt & geojson datasets.`)
}
+ // Set default layer params if nullish.
layer.params ??= {}
- // If layer configuration is wrong and contains both cluster.distance and cluster.resolution, error and return
- if (layer.cluster?.distance && layer.cluster?.resolution) {
-
- console.error(`Layer: ${layer.key}, cluster.distance and cluster.resolution are mutually exclusive. You cannot use them both on the same layer. Please remove one of them. `)
-
- return;
- };
-
- if (layer.cluster?.resolution) {
- layer.format = 'cluster';
- layer.params.viewport = true;
- layer.params.z = true;
- layer.params.resolution = layer.cluster.resolution;
- layer.params.template = layer.cluster.hexgrid ? 'cluster_hex' : 'cluster';
- }
+ clusterConfig(layer)
// Assign style object if nullish.
- layer.style ??= {}
-
- layer.srid ??= '4326'
+ layer.style ??= {};
layer.setSource = (features) => {
@@ -106,6 +90,7 @@ export default layer => {
locale: layer.mapview.locale.key,
layer: layer.key,
table,
+ srid: layer.srid,
filter: layer.filter?.current,
...layer.params
})}`)
@@ -144,19 +129,11 @@ export default layer => {
}
// Change method for the cluster feature properties and layer stats.
- layer.L.on('change', e => {
-
- // Do not process cluster for non cluster layers.
- if (!layer.cluster) return;
+ layer.cluster?.distance && layer.L.on('change', e => {
// To prevent layer.L.change() from crashing if called before data is loaded.
if (!layer.cluster.source) return;
- // The OL cluster must not be processed with a resolution.
- if (layer.cluster.resolution) return;
-
- if (!layer.cluster?.distance) return;
-
delete layer.max_size;
const feature_counts = layer.cluster.source.getFeatures().map(F => {
@@ -191,5 +168,53 @@ export default layer => {
// Calculate max_size for cluster size styling.
layer.max_size = Math.max(...feature_counts)
})
+}
+
+function clusterConfig(layer) {
+ // The clusterConfig can not work without the layer having a cluster config object.
+ if (typeof layer.cluster !== 'object') return;
+
+ // Check if both cluster.distance and cluster.resolution are set.
+ if (layer.cluster.distance && layer.cluster.resolution) {
+ console.warn(`Layer: ${layer.key}, cluster.distance and cluster.resolution are mutually exclusive. You cannot use them both on the same layer. Please remove one of them. `)
+ return;
+ };
+
+ // Check if neither cluster.distance and cluster.resolution are set.
+ if (!layer.cluster.distance && !layer.cluster.resolution) {
+ console.warn(`Layer: ${layer.key}, cluster.distance or cluster.resolution must be set.`)
+ return;
+ };
+
+ // If cluster.resolution is used, the layer srid must be set to 3857.
+ if (layer.cluster.resolution) {
+
+ // Check if resolution is numeric.
+ if (typeof layer.cluster.resolution === 'number') {
+ // Assign resolution as float.
+ layer.params.resolution = parseFloat(layer.cluster.resolution);
+ }
+ // Otherwise, warn and return.
+ else {
+ console.warn(`Layer: ${layer.key}, cluster.resolution must be a number.`)
+ return;
+ }
+
+ // Check if srid is set to 4326, not allowed for cluster layer
+ if (layer.srid === '4326') {
+ console.warn(`Layer: ${layer.key}, srid 4326 is not allowed for cluster.resolution layers.`)
+ return;
+ };
+
+ // Provide default params for resolution cluster.
+ layer.params.viewport = true;
+ layer.params.z = true;
+
+ // Format is cluster if resolution is set.
+ layer.format = 'cluster';
+
+ // Assign default template.
+ layer.params.template ??= layer.cluster.hexgrid ? 'cluster_hex' : 'cluster';
+ }
}
\ No newline at end of file
diff --git a/lib/layer/themes/distributed.mjs b/lib/layer/themes/distributed.mjs
index a6292224a..24761004e 100644
--- a/lib/layer/themes/distributed.mjs
+++ b/lib/layer/themes/distributed.mjs
@@ -1,31 +1,29 @@
export default function(theme, feature) {
if (!theme.lookup) {
-
theme.lookup = {}
theme.boxes = []
theme.index = 0
}
+ // Get feature identifier for theme.
+ const ID = feature.properties[theme.field || 'id'] || 0
+
// The feature field property value already has a style assigned.
- if (theme.lookup[feature.properties[theme.field]]) {
+ if (theme.lookup[ID]) {
// Assign style from lookup object.
- var catStyle = theme.lookup[feature.properties[theme.field]]
- mapp.utils.merge(feature.style, catStyle)
+ mapp.utils.merge(feature.style, theme.lookup[ID])
return;
}
// Get feature bounding box from geometry extent.
- let bbox = {
+ const bbox = {
extent: feature.getGeometry().getExtent()
}
- // add box to visual check layer.
- // theme.source.addFeature(new ol.Feature(new ol.geom.Polygon.fromExtent(bbox.extent)))
-
// Find intersecting bounding boxes with their assigned cat index.
- let intersects = theme.boxes.filter(b => !(bbox.extent[0] > b.extent[2]
+ const intersects = theme.boxes.filter(b => !(bbox.extent[0] > b.extent[2]
|| bbox.extent[2] < b.extent[0]
|| bbox.extent[1] > b.extent[3]
|| bbox.extent[3] < b.extent[1]))
@@ -34,23 +32,24 @@ export default function(theme, feature) {
theme.boxes.push(bbox)
// Create a set of cat indices from intersecting bounding boxes.
- let set = new Set(intersects.map(b => b.themeIdx))
+ const set = new Set(intersects.map(b => b.themeIdx))
- // Increase the cat indix.
+ // Increase the current cat indix.
theme.index++
// Reset cat index to 0 if the index reaches the length of the cat array.
if (theme.index === theme.cat_arr.length) theme.index = 0
- // Check whether the set of intersecting bounding boxes has NOT the cat index.
+ // i is the cat index if not in set of intersecting boxes.
let i = !(set.has(theme.index)) && parseInt(theme.index)
- // Index is not available.
+ // Current index is already in set of intersecting boxes.
if (i === false) {
// Iterate through the cat array.
for (let free = 0; free < theme.cat_arr.length; free++) {
+ // Find an index which is not in set of intersecting bbox indices.
if (!set.has(free)) {
// Assign free index and break loop.
@@ -60,22 +59,19 @@ export default function(theme, feature) {
}
}
- // No index is available.
+ // Any index is in set of intersecting box indices.
if (i === false) {
- // Assign cat index.
+ // Just assign the current index. It is not possible to prevent some neighbouring cats.
i = parseInt(theme.index)
}
// Assign index to the bounding box which is stored in the array of bounding boxes.
bbox.themeIdx = i
- // Get the JSON style from cat array via index.
- var catStyle = theme.cat_arr[i]
-
- // Assign the JSON style to the lookup object for the feature field property value.
- theme.lookup[feature.properties[theme.field]] = catStyle
+ // Assign the style to the lookup object for the feature field property value.
+ theme.lookup[ID] = theme.cat_arr[i]
// Merge the cat style with the feature style.
- mapp.utils.merge(feature.style, catStyle)
+ mapp.utils.merge(feature.style, theme.lookup[ID])
}
\ No newline at end of file
diff --git a/lib/mapp.mjs b/lib/mapp.mjs
index 0c8879556..afc8b0b3b 100644
--- a/lib/mapp.mjs
+++ b/lib/mapp.mjs
@@ -18,7 +18,7 @@ self.mapp = (function (mapp) {
Object.assign(mapp, {
version: '4.7.2',
- hash: 'f836737e4c92e114f788dc6461ffcd5ecddba295',
+ hash: '4026463d6e57c02f0da8ad676d81f59e47baacc8',
language: hooks.current.language || 'en',
dictionaries,
diff --git a/lib/mapview/infotip.mjs b/lib/mapview/infotip.mjs
index 5c5977aa5..8b2234ec8 100644
--- a/lib/mapview/infotip.mjs
+++ b/lib/mapview/infotip.mjs
@@ -7,20 +7,42 @@ export default function(content) {
// Remove infotip node element
infotip.node?.remove()
+ // The mapview must have a position to place the infotip.
+ if (!mapview.position) return;
+
// Remove infotip positioning event from mapview Map.
mapview.Map.un('pointermove', position)
// Just clears the infotip.
if (!content) return;
- // Creates the infotip node.
- infotip.node = mapp.utils.html.node`
`
+ if (content instanceof HTMLElement) {
+
+ infotip.node = content
+
+ // Content type object but not HTMLElement cannot be rendered.
+ } else if (typeof content === 'object') {
+
+ console.log(content)
+ return;
+
+ } else {
- // Assigns the infotip content.
- infotip.node.innerHTML = content
+ // Check for braces in content string.
+ if (/[()]/.test(content)) {
+
+ console.warn(`Potential XSS detected in infotip content ${content}`)
+ }
+
+ // Creates the infotip node.
+ infotip.node = mapp.utils.html.node`
`
+
+ // Assigns the infotip content.
+ infotip.node.innerHTML = content
+ }
// Appends the infotip to the mapview.Map.
- mapview.Map.getTargetElement().appendChild(infotip.node)
+ mapview.Map.getTargetElement().append(infotip.node)
// Assign infotip positioning event to mapview.Map.
mapview.Map.on('pointermove', position)
@@ -37,5 +59,4 @@ export default function(content) {
infotip.node.style.left = mapview.pointerLocation.x - infotip.node.offsetWidth / 2 + 'px'
infotip.node.style.top = mapview.pointerLocation.y - infotip.node.offsetHeight - 15 + 'px'
}
-
}
\ No newline at end of file
diff --git a/lib/ui/Dataview.mjs b/lib/ui/Dataview.mjs
index b9a200de1..9d2779dd1 100644
--- a/lib/ui/Dataview.mjs
+++ b/lib/ui/Dataview.mjs
@@ -15,12 +15,12 @@ export default async (_this) => {
}
// Assign queryparams from layer, locale.
- _this.queryparams = Object.assign(
- _this.queryparams || {},
- _this.layer?.queryparams || {},
- _this.layer?.mapview.locale.queryparams || {},
- _this.location?.layer?.queryparams || {},
- _this.location?.layer?.mapview.locale.queryparams || {})
+ _this.queryparams = {
+ ..._this.queryparams,
+ ..._this.layer?.queryparams,
+ ..._this.layer?.mapview?.locale?.queryparams,
+ ..._this.location?.layer?.queryparams,
+ ..._this.location?.layer?.mapview?.locale?.queryparams}
// Update method for _this.
_this.update = async () => {
@@ -75,7 +75,7 @@ export default async (_this) => {
}
// Create a ChartJS dataview is chart is defined.
- if (_this.chart) await Chart(_this);
+ if (_this.chart) await mapp.ui.utils.Chart(_this);
// Columns in entry indicate missing table config.
if (typeof _this.columns !== 'undefined') {
@@ -86,14 +86,14 @@ export default async (_this) => {
}
// Create a Tabulator dataview if columns are defined.
- if (_this.table) await Table(_this);
+ if (_this.table) await mapp.ui.utils.Tabulator(_this);
// Update the dataview on mapChange if set.
- _this.mapChange &&
- _this.layer &&
- _this.layer.mapview.Map.getTargetElement().addEventListener(
- 'changeEnd',
- () => {
+ _this.mapChange
+ && _this.layer
+ && _this.layer.mapview.Map.getTargetElement()
+ .addEventListener('changeEnd', () => {
+
// Only update dataview if corresponding layer is visible.
if (_this.layer && !_this.layer.display) return;
@@ -103,209 +103,7 @@ export default async (_this) => {
// Execute mapChange if defined as function or dataview update method.
(typeof _this.mapChange === 'function' && _this.mapChange()) ||
_this.update();
- }
- );
+ });
return _this;
-};
-
-async function Chart(_this) {
- // Charts most be rendered into a canvas type element.
- const canvas = _this.target.appendChild(mapp.utils.html.node`