diff --git a/.gitignore b/.gitignore index 5eb826d30f..681bcc2ce9 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,5 @@ public/js/* !tests/** !package.json !jsdoc*.json -!codi.json \ No newline at end of file +!codi.json +!.prettierrc.json diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000000..945ffaf26d --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,15 @@ +{ + "printWidth": 80, + "tabWidth": 2, + "semi": true, + "singleQuote": true, + "quoteProps": "as-needed", + "jsxSingleQuote": false, + "bracketSpacing": true, + "bracketSameLine": false, + "arrowParens": "always", + "proseWrap": "preserve", + "htmlWhitespaceSensitivity": "css", + "endOfLine": "lf", + "embeddedLanguageFormatting": "auto" +} diff --git a/DEVELOPING.md b/DEVELOPING.md index d8aecda64c..4ac0380a73 100644 --- a/DEVELOPING.md +++ b/DEVELOPING.md @@ -9,8 +9,8 @@ You will start by The minimum requirements are: -* Git -* [Node.js](https://nodejs.org/) (version 18 and above) +- Git +- [Node.js](https://nodejs.org/) (version 18 and above) The executables `git` and `node` should be in your `PATH`. @@ -44,51 +44,42 @@ The development environment uses nodemon to watch for changes and automatically ```json { - "watch": [ - "lib/**/*", - "tests/**/*", - "public/css/*", - "../xyz_resources/**/*" - ], - "ext": ".js,.mjs,.json,.css,.svg", - "ignore": [ - "public/js/**/*", - "public/css/mapp.css", - "public/css/ui.css" - ], - "env": { - "NODE_ENV": "DEVELOPMENT" - }, - "exec": "npx concurrently \"node esbuild.config.mjs\" \"npm run ui_css\" \"npm run mapp_css\"", - "events": { - "start": "echo \"Watching for changes...\"", - "exit": "echo \"Build complete\"" - } + "watch": ["lib/**/*", "tests/**/*", "public/css/*", "../xyz_resources/**/*"], + "ext": ".js,.mjs,.json,.css,.svg", + "ignore": ["public/js/**/*", "public/css/mapp.css", "public/css/ui.css"], + "env": { + "NODE_ENV": "DEVELOPMENT" + }, + "exec": "npx concurrently \"node esbuild.config.mjs\" \"npm run ui_css\" \"npm run mapp_css\"", + "events": { + "start": "echo \"Watching for changes...\"", + "exit": "echo \"Build complete\"" + } } ``` #### Watched Directories -* `lib/**/*`: All files in the lib directory -* `tests/**/*`: All test files -* `public/css/*`: CSS source files -* `../xyz_resources/**/*`: External resource files. +- `lib/**/*`: All files in the lib directory +- `tests/**/*`: All test files +- `public/css/*`: CSS source files +- `../xyz_resources/**/*`: External resource files. #### File Types Watched -* JavaScript files (`.js`) -* ES Modules (`.mjs`) -* JSON files (`.json`) -* CSS files (`.css`) -* SVG files (`.svg`) +- JavaScript files (`.js`) +- ES Modules (`.mjs`) +- JSON files (`.json`) +- CSS files (`.css`) +- SVG files (`.svg`) #### Ignored Files -* Built JavaScript files (`public/js/**/*`) +- Built JavaScript files (`public/js/**/*`) -* Compiled CSS files: - * `public/css/mapp.css` - * `public/css/ui.css` +- Compiled CSS files: + - `public/css/mapp.css` + - `public/css/ui.css` #### Automatic Actions @@ -108,23 +99,25 @@ When changes are detected: ``` 2. Nodemon will: - * Set `NODE_ENV` to "DEVELOPMENT" - * Watch for file changes - * Automatically rebuild affected files - * Display "Watching for changes..." when started - * Show "Build complete" after each rebuild + + - Set `NODE_ENV` to "DEVELOPMENT" + - Watch for file changes + - Automatically rebuild affected files + - Display "Watching for changes..." when started + - Show "Build complete" after each rebuild 3. The application will rebuild automatically when you: - * Modify test files - * Change source code - * Update CSS - * Modify resources + - Modify test files + - Change source code + - Update CSS + - Modify resources This ensures that your test environment always has the latest changes without manual rebuilds. ### VSCode Tasks & Launch A task can be added to the `.vscode/tasks.json` to execute `nodemon` and `browser-sync` concurrently. This will allow VSCode to rebuild the application on script changes in the editor. +Along side this there is an optional `kill-watch` task that is used to tear down the `start-watch` process once finished with debugging. ```json { @@ -145,12 +138,17 @@ A task can be added to the `.vscode/tasks.json` to execute `nodemon` and `browse "endsPattern": "Build complete" } } + }, + { + "label": "kill-watch", + "type": "shell", + "command": "pkill -f nodemon; pkill -f browser-sync" } ] } ``` -`start-watch` is a `preLaunchTask` which must be added to the debug configuration in the `.vscode/launch.json`. +`start-watch` (preLaunchTask) and `kill-watch` (postDebugTask) must be added to your debug configuration in `.vscode/launch.json` to ensure the watch process starts before debugging and stops after each debugging session. ```json { @@ -161,6 +159,7 @@ A task can be added to the `.vscode/tasks.json` to execute `nodemon` and `browse "preLaunchTask": "start-watch", "console": "integratedTerminal", "internalConsoleOptions": "openOnSessionStart", + "postDebugTask": "kill-watch", "env": {} } ``` @@ -240,6 +239,18 @@ eslint command with fix There are other extensions you can use in your editor to get on the fly error highlighting where any rules are broken. Please look into what eslint supports in your environment. +## Prettier.io + +For formatting we have implemented [prettier.io](prettier.io). +This is so we can ensure that there is consistent formatting across xyz. + +To get prettier.io working in your editor you will need to follow one of the setups below: + +- [VSCode](https://prettier.io/docs/en/editors#visual-studio-code) +- [Sublime](https://prettier.io/docs/en/editors#sublime-text) +- [Atom](https://prettier.io/docs/en/editors#atom) +- [Vim](https://prettier.io/docs/en/editors#vim) + ## version.js hash The mapp module object holds a hash of the latest release commit which can be generated by executing the version.js script in the root. diff --git a/README.md b/README.md index 4b9f0f6a3d..532d519d81 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -**v4.13.0-alpha** +**v4.13.0-beta** [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) ![Codi Unit Tests](https://github.com/GEOLYTIX/xyz/actions/workflows/unit_tests.yml/badge.svg) diff --git a/api/api.js b/api/api.js index 2b3191217d..9f3a98a1de 100644 --- a/api/api.js +++ b/api/api.js @@ -104,7 +104,7 @@ const routes = { process.env.COOKIE_TTL ??= '36000' -process.env.TITLE ??= 'GEOLYTIX | XYZ' +process.env.TITLE ??= 'XYZ | MAPP' process.env.DIR ??= '' diff --git a/eslint.config.mjs b/eslint.config.mjs index 832ba58c24..5a624d6ea1 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -15,7 +15,7 @@ export default [ 'max': 4 } ], - //'complexity': ['error', { 'max': 15 }], + // 'complexity': ['error', { 'max': 15 }], 'no-nested-ternary': 'error' } } diff --git a/lib/layer/decorate.mjs b/lib/layer/decorate.mjs index 0a3dad0e67..a48612e344 100644 --- a/lib/layer/decorate.mjs +++ b/lib/layer/decorate.mjs @@ -27,6 +27,7 @@ The layer object is returned from the decorator. The layer decorator method create mapp-layer typedef object from a json-layer. @param {object} layer JSON layer. +@property {boolean} layer.featureLocation Locations will be gotten from layer.features. @returns {layer} Decorated Mapp Layer. */ @@ -97,6 +98,9 @@ export default async function decorate(layer) { .forEach(entry => entry.skipEntry = true) } + //Remove edit property from infoj entries on featureLocation layer. + layer.featureLocation && layer?.infoj?.forEach(entry => delete entry.edit); + // Call layer and/or plugin methods. Object.keys(layer).forEach((key) => { typeof mapp.layer[key] === 'function' && mapp.layer[key]?.(layer); diff --git a/lib/layer/format/maplibre.mjs b/lib/layer/format/maplibre.mjs index 7660392ae9..2f13902e1c 100644 --- a/lib/layer/format/maplibre.mjs +++ b/lib/layer/format/maplibre.mjs @@ -54,7 +54,7 @@ export default async layer => { layer.mapview.Map.getTargetElement().prepend(layer.container) - layer.Map = await MaplibreGL({ + const maplibreMap = await MaplibreGL({ container: layer.container, pixelRatio: 1, style: layer.style.URL, @@ -80,10 +80,10 @@ export default async layer => { } }); - if (!layer.Map) return; + if (!Map) return; // The Maplibre Map control must resize with mapview Map targetElement. - layer.mapview.Map.getTargetElement().addEventListener('resize', () => layer.Map.resize()) + layer.mapview.Map.getTargetElement().addEventListener('resize', () => maplibreMap.resize()) // Handle layer.style.zIndex deprecation. if (layer.style.zIndex) { @@ -100,7 +100,7 @@ export default async layer => { layer.container.style.visibility = 'visible'; // adjust view parameters in mapbox - layer.Map.jumpTo({ + maplibreMap.jumpTo({ center: ol.proj.toLonLat(frameState.viewState.center), zoom: frameState.viewState.zoom - 1, bearing: (-frameState.viewState.rotation * 180) / Math.PI, diff --git a/lib/layer/format/mvt.mjs b/lib/layer/format/mvt.mjs index ed72a3aac7..d2eccadfb4 100644 --- a/lib/layer/format/mvt.mjs +++ b/lib/layer/format/mvt.mjs @@ -84,8 +84,6 @@ export default function MVT(layer) { layer.source.clear() layer.source.refresh() - // Reset source tiles to force refresh. - layer.source.sourceTiles_ = {}; layer.featureSource.refresh() if (callback instanceof Function) callback(layer) diff --git a/lib/layer/styleParser.mjs b/lib/layer/styleParser.mjs index 1201a08ee4..b25b3835cf 100644 --- a/lib/layer/styleParser.mjs +++ b/lib/layer/styleParser.mjs @@ -1,38 +1,7 @@ /** -### mapp.layer.styleParser() +### /layer/styleParser -The `styleParser` module method is responsible for parsing and validating the layer.style object for assigning feature styles. - -The main tasks performed by the `styleParser` module include: - -1. Assigning default styles: - - It assigns a default highlight style to the layer if not explicitly provided. - - It sets the default `zIndex` value for the highlight style to `Infinity`. - - It assigns an empty object to `layer.style` if it doesn't exist. - - It assigns an empty object to `layer.style.default` if it doesn't exist. - -2. Parsing theme styles: - - If `layer.style.theme` exists, it calls the `parseTheme` function to process the theme style configuration. - - If `layer.style.themes` exists, it iterates over each theme and calls the `parseTheme` function for each theme. - -3. Handling multiple themes, hovers, and labels: - - If multiple themes are defined in `layer.style.themes`, it selects the appropriate theme based on the `layer.style.theme` value or the first theme in the object. - - If multiple hovers are defined in `layer.style.hovers`, it selects the appropriate hover style based on the `layer.style.hover` value or the first hover style in the object. - - If multiple labels are defined in `layer.style.labels`, it selects the appropriate label style based on the `layer.style.label` value or the first label style in the object. - -4. Handling warnings and deprecation notices: - - It calls the `warnings` function to handle warnings and deprecation notices related to the layer style configuration. - - It checks for deprecated properties and provides appropriate warnings or fallback values. - -5. Processing style objects: - - It calls the `styleObject` function to process individual style objects within themes or categories. - - It merges the category style with the default style and handles icon styles separately. - -6. Processing icon style objects: - - It calls the `iconObject` function to process icon style objects within the layer style. - - It moves icon-related properties into a separate `icon` object within the style. - -Overall, the `styleParser` module ensures that the layer style configuration is properly structured, applies default styles where necessary, handles multiple themes, hovers, and labels, and processes individual style objects and icon styles. It helps to maintain a consistent and valid style configuration for the layer in the mapping application. +The styleParser module exports the styleParser method as default which is intended to check the consistency of layer styles and issue console warnings in regards to backwards compatibility. @module /layer/styleParser */ @@ -79,8 +48,25 @@ const styleKeys = new Set([ @description The styleParser method parses and validates the mapp.style object and its properties. +The styleParser method checks the highlight features style and calls the warnings method. + +The clusterStyle method is called to ensure that cluster layer have a cluster style. + +Individual themes in the themes object are parsed and a theme is assigned to the + +The parseTheme method is called for the layer.style.theme and each of the layer.style.themes. + +A lookup will be attempted if the theme property is a string. Otherwise the first theme object property from the themes object is assigned as theme if undefined. + +A lookup will be attempted if the hover property is a string. Otherwise the first hover object property from the hovers object is assigned as hover if undefined. + +A lookup will be attempted if the label property is a string. Otherwise the first label object property from the labels object is assigned as label if undefined. + @param {layer} layer A json layer object. @property {layer-style} layer.style The mapp-layer style configuration. +@property {feature-style} [style.highlight] The feature style applied to features by the highlight interaction. +@property {object} [style.theme] The current theme applied to the feature styling. +@property {array} [style.themes] An array of theme objects available to be applied as the layer.style.theme. */ export default function styleParser(layer) { @@ -98,346 +84,468 @@ export default function styleParser(layer) { clusterChecks(layer) - if (layer.style?.theme) { - parseTheme(layer.style.theme, layer) - } + parseTheme(layer.style.theme, layer) if (layer.style?.themes) { + Object.keys(layer.style.themes).forEach(key => { + + // required for the lookup of self referenced objects + layer.style.themes[key].key = key + + if (layer.style.themes[key].skip) { + delete layer.style.themes[key]; + return; + } + + // assign key as fallback title + layer.style.themes[key].title ??= key; + parseTheme(layer.style.themes[key], layer) }) - } - // Handle multiple themes in layer style. - if (layer.style?.themes) { - Object.keys(layer.style.themes).forEach(key => { - layer.style.themes[key].title ??= key; - if (layer.style.themes[key].skip) delete layer.style.themes[key]; - }); + // Assign the first key from themes object as theme string property if undefined. + layer.style.theme ??= Object.keys(layer.style.themes)[0] - layer.style.theme = typeof layer.style.theme === 'object' - ? layer.style.theme - : layer.style.themes[layer.style.theme || Object.keys(layer.style.themes)[0]]; - } + // Assign theme property from themes object if string. + if (typeof layer.style.theme === 'string') { - // Handle multiple hovers in layer style. - if (layer.style?.hovers) { - layer.style.hover = typeof layer.style.hover === 'object' ? layer.style.hover : layer.style.hovers[layer.style.hover || Object.keys(layer.style.hovers)[0]]; + layer.style.theme = layer.style.themes[layer.style.theme] + } } - // Set default featureHover method if not provided. - if (layer.style?.hover) { - layer.style.hover.method ??= mapp.layer.featureHover; - } + handleHovers(layer); // Handle multiple labels in layer style. if (layer.style?.labels) { - layer.style.label = typeof layer.style.label === 'object' ? layer.style.label : layer.style.labels[layer.style.label || Object.keys(layer.style.labels)[0]]; - } - function warnings(layer) { + Object.keys(layer.style.labels).forEach(key => { + + // required for the lookup of self referenced objects + layer.style.labels[key].key = key - if (!layer.style.default) { + // assign key as fallback title + layer.style.labels[key].title ??= key; + }) - console.warn(`Layer: ${layer.key} has no implicit default style. Please add style.default.`) + // Assign the first key from labels object as label string property if undefined. + layer.style.label ??= Object.keys(layer.style.labels)[0] - // Assign default style for vector layer. - // Non cluster layer do not have a default icon style - layer.style.default = layer.cluster - ? { - icon: { - type: 'dot' - } - } : { - strokeColor: '#333', - fillColor: '#fff9', - } + + // Assign label property from labels object if string. + if (typeof layer.style.label === 'string') { + + layer.style.label = layer.style.labels[layer.style.label] } + } +} - if (layer.style.default.style) { +/** +@function warnings - console.warn(`Layer: ${layer.key} has a style object within the default style configuration.`) +@description +The warnings method parses the layer style configuration and warns on legacy configurations while trying to rectify these issues. - layer.style.default = layer.style.default.style +The method ensures that the layer-style object has a default feature-style. The default feature-style must have an icon if the layer is a cluster layer. - delete layer.style.default.style - } +The default is a feature-style which must not contain a style object property. - iconObject(layer.style.default) +The icon object of the default style is checked. - // Handle deprecated layer.hover configuration. - if (layer.hover) { - console.warn(`Layer: ${layer.key}, layer.hover{} should be defined within layer.style{}.`); - layer.style.hover = layer.hover; - delete layer.hover; - } +The layer.hover legacy configuration is checked. - // Handle deprecated layer.style.hover and layer.style.hovers. - 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; - } +The layer.style.zIndex legacy configuration is checked. - // Handle deprecated layer.style.label and layer.style.labels. - 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; - } +The layer.icon_scaling legacy configuartion is checked. - // Handle layer.style.zIndex deprecation. - if (layer.style.zIndex) { - console.warn(`Layer: ${layer.key}, layer.style.zIndex has been deprecated. Use layer.zIndex instead.`); - } +@param {layer} layer A json layer object. +@property {Object} [layer.cluster] Cluster configuration for a point layer. +@property {Object} [layer.hover] Legacy configuration for style.hover. +@property {Object} [layer.icon_scaling] Legacy configuration for style.icon_scaling. +@property {layer-style} layer.style The mapp-layer style configuration. +@property {feature-style} style.default Default feature style. +@property {integer} [style.zIndex] Legacy configuration for layer.zIndex. +*/ +function warnings(layer) { - if (layer.icon_scaling) { + if (!layer.style.default) { - console.warn(`Layer: ${layer.key}, layer.icon_scaling has been assigned to layer.style.icon_scaling`) + console.warn(`Layer: ${layer.key} has no implicit default style. Please add style.default.`) - layer.style.icon_scaling ??= layer.icon_scaling - } + // Assign default style for vector layer. + // Non cluster layer do not have a default icon style + layer.style.default = layer.cluster + ? { + icon: { + type: 'dot' + } + } : { + strokeColor: '#333', + fillColor: '#fff9', + } } - function parseTheme(theme, layer) { + // The default is a feature-style which must not contain a style object property. + if (layer.style.default.style) { - if (typeof theme === 'string') { + console.warn(`Layer: ${layer.key} has a style object within the default style configuration.`) - // Attempt theme lookup in themes from string[key] - theme = layer.style.themes?.[theme] - } + layer.style.default = layer.style.default.style - if (typeof theme.style === 'object') { + delete layer.style.default.style + } - // Assign the default style to the theme.style - theme.style = { - ...structuredClone(layer.style.default), - ...theme.style - } - } + iconObject(layer.style.default) - if (typeof theme.cat === 'object') { + // Handle legacy layer.hover configuration. + if (layer.hover) { + console.warn(`Layer: ${layer.key}, layer.hover{} should be defined within layer.style{}.`); + layer.style.hover = layer.hover; + delete layer.hover; + } - theme.categories = Object.keys(theme.cat).map(key => { + // Handle legacy layer.style.zIndex configuration. + if (layer.style.zIndex) { + console.warn(`Layer: ${layer.key}, layer.style.zIndex has been deprecated. Use layer.zIndex instead.`); + } - const cat = theme.cat[key] + // Handle legacy layer.icon_scaling configuration. + if (layer.icon_scaling) { - cat.label ??= key - cat.value ??= key + console.warn(`Layer: ${layer.key}, layer.icon_scaling has been assigned to layer.style.icon_scaling`) - return cat - }) + layer.style.icon_scaling ??= layer.icon_scaling + } +} - delete theme.cat - } +/** +@function clusterChecks - if (Array.isArray(theme.cat_arr)) { +@description +The clusterChecks styleParser module method checks the style configuration for a cluster layer. - theme.categories = theme.cat_arr +Cluster layer are by defintion point layer and must have style.default.icon to represent point feature geometries. - delete theme.cat_arr - } +Other vector geometries can not be displayed in a cluster feature layer. Stroke and fill styles will be removed from the style.default{} configuration. - // Check if graduated breaks is not defined, or is not less_than or greater_than. - if (theme.type === 'graduated') { +The style.cluster{} configuration will be spread into a default cluster style object with clusterScale=1. - if (!['less_than', 'greater_than'].includes(theme.graduated_breaks)) { +zoomInScale and zoomOutScale may apply to point features which are not cluster features and are moved to the layer.style. - console.warn(`You must provide a graduated_breaks value of either greater_than or less_than for graduated theme on layer: ${layer.key}; field: ${theme.field}. less_than is assumed.`); +@param {layer} layer A json layer object. +@property {layer-style} layer.style The mapp-layer style configuration. +@property {Object} layer.cluster Cluster configuration for a point layer. +@property {feature-style} style.default Default feature style. +@property {feature-style} style.cluster Style for cluster feature. +@property {feature-style} style.selected Style for features of selected locations. +*/ +function clusterChecks(layer) { - theme.graduated_breaks ??= 'less_than'; - } + if (!layer.cluster) return - if (theme.graduated_breaks === 'greater_than') { + if (!layer.style.default.icon) { - // The cat array must be reversed when checking whether a value is supposed to be greater. - theme.categories.reverse() + layer.style.default = { + icon: { + type: 'dot' } } - theme.categories?.forEach(cat => { + console.warn(`Cluster Layer: ${layer.key} has no default icon. 'Dot' will be assigned.`) + } - cat.label ??= cat.value + // Cluster layer must not have stroke or fill styles. + Object.keys(layer.style.default) + .filter(key => !['icon', 'scale'].includes(key)) + .forEach(key => { - if (cat.icon) { + console.warn(`Cluster Layer: ${layer.key}; ${key} key removed from default style.`) - cat.style = { - icon: cat.icon - } + delete layer.style.default[key] + }) - delete cat.icon - } + // Define default style cluster icon + layer.style.cluster = { + clusterScale: 1, + icon: { + type: 'dot' + }, + ...layer.style.cluster + } - styleObject(cat, structuredClone(layer.style.default)) - }) + if (layer.style.cluster.zoomInScale) { + + layer.style.zoomInScale = layer.style.cluster.zoomInScale - // Check validity of categorized theme with multiple fields. - if (theme.type === 'categorized' && Array.isArray(theme.fields)) { + delete layer.style.cluster.zoomInScale + } - theme.categories.forEach(cat => { + if (layer.style.cluster.zoomOutScale) { - if (!theme.fields.includes(cat.field)) { + layer.style.zoomOutScale = layer.style.cluster.zoomOutScale - console.warn(`Layer: ${layer.key}; Cat ${cat.label} missed valid field.`) - } + delete layer.style.cluster.zoomOutScale + } +} - // Multiple field cat theme style must be icon. - if (!cat.style.icon) { +/** +@function parseTheme - console.warn(`Layer: ${layer.key}; Cat ${cat.label} has invalid icon style.`) +@description +The parseTheme method checks whether a theme and it's categories have consistent style objects. - cat.style.icon = { type: 'dot' } - } - }) +@param {Object} theme A json theme object. +@param {layer} layer A json layer object. +@property {string} theme.type The type of the theme. +@property {array} theme.categories An ordered array of theme categories. +@property {layer-style} layer.style The mapp-layer style configuration. +@property {feature-style} style.default Default feature style. +*/ +function parseTheme(theme, layer) { + if (typeof theme !== 'object') return; + + if (typeof theme.style === 'object') { + + // Assign the default style to the theme.style + theme.style = { + ...structuredClone(layer.style.default), + ...theme.style } } - function styleObject(cat, defaultStyle) { + if (typeof theme.cat === 'object') { + + theme.categories = Object.keys(theme.cat).map(key => { + + const cat = theme.cat[key] + + cat.label ??= key + cat.value ??= key + + return cat + }) + + delete theme.cat + } + + if (Array.isArray(theme.cat_arr)) { + + theme.categories = theme.cat_arr + + delete theme.cat_arr + } + + graduatedTheme(theme) - cat.style ??= {} + theme.categories?.forEach(cat => { - // Style arrays are assumed to be valid. - if (Array.isArray(cat.style)) return; + cat.label ??= cat.value if (cat.icon) { + cat.style = { icon: cat.icon } + delete cat.icon } - if (cat.style.icon) { - - // Do not merge default style into icon array. - if (Array.isArray(cat.style.icon)) return; + catStyle(cat, layer) + }) - // Do not merge default style into icon with type definition. - if (cat.style.icon.type) return; + // Check validity of categorized theme with multiple fields. + if (theme.type === 'categorized' && Array.isArray(theme.fields)) { - // Do not merge default style into svg [type] icons. - if (cat.style.icon.svg) return; + theme.categories.forEach(cat => { - if (defaultStyle.icon && !Array.isArray(defaultStyle.icon)) { + if (!theme.fields.includes(cat.field)) { - cat.style.icon = { - ...defaultStyle.icon, - ...cat.style.icon - } + console.warn(`Layer: ${layer.key}; Cat ${cat.label} missed valid field.`) } - return; - } + // Multiple field cat theme style must be icon. + if (!cat.style.icon) { - // Create a mergeStyle from valid styleKeys - const mergeStyle = {} + console.warn(`Layer: ${layer.key}; Cat ${cat.label} has invalid icon style.`) - Object.keys(defaultStyle) - .filter(key => styleKeys.has(key)) - .forEach(key => mergeStyle[key] = defaultStyle[key]) + cat.style.icon = { type: 'dot' } + } + }) - cat.style = { - ...mergeStyle, - ...cat.style, - } + } +} - Object.keys(cat) - .filter(key => styleKeys.has(key)) - .forEach(key => { - cat.style[key] = cat[key] - delete cat[key] - }) +/** +@function graduatedTheme - iconObject(cat.style) +@description +The checks the order of theme categories according to their value. The method will shortcircuit if the type of the theme is not graduated. - // Assign default icon if no cat style icon could be created. - if (!cat.style.icon && defaultStyle.icon) { +@param {Object} theme A json theme object. +@param {layer} layer A json layer object. +@property {string} theme.type The type of the theme. +@property {string} theme.graduated_breaks The value order of categories, eg. 'less_than' or 'greater_than' +@property {array} theme.categories An ordered array of theme categories. +*/ +function graduatedTheme(theme) { - console.warn(`Layer:${layer.key}, Failed to evaluate icon: ${JSON.stringify(cat)}. Default icon will be assigned.`) + if (theme.type !== 'graduated') return; - cat.style.icon = defaultStyle.icon - } + if (!['less_than', 'greater_than'].includes(theme.graduated_breaks)) { + + theme.graduated_breaks = 'less_than'; } - function iconObject(style) { + theme.categories.forEach(cat => cat.value = Number(cat.value)) + + if (theme.graduated_breaks === 'less_than') { - // The style object already has an icon object. - if (style.icon) return; + theme.categories.sort((a, b) => (a.value > b.value ? 0 : -1)); + } else { - Object.keys(style) - .filter(key => iconKeys.has(key)) - .forEach(key => { - style.icon ??= {}; - style.icon[key] = style[key] - delete style[key] - }) + theme.categories.sort((a, b) => (a.value > b.value ? -1 : 0)); } } /** -@function clusterChecks +@function catStyle @description -The clusterChecks styleParser module method checks the style configuration for a cluster layer. +The catStyle method parses the style object of a theme category object. -Cluster layer are by defintion point layer and must have style.default.icon to represent point feature geometries. +Category themes are attempted to be merged with the default style if possible. -Other vector geometries can not be displayed in a cluster feature layer. Stroke and fill styles will be removed from the style.default{} configuration. +@param {object} cat The category object. +@param {layer} layer The layer reference for the style/theme/cat object. +*/ +function catStyle(cat, layer) { -The style.cluster{} configuration will be spread into a default cluster style object with clusterScale=1. + cat.style ??= {} -zoomInScale and zoomOutScale may apply to point features which are not cluster features and are moved to the layer.style. + // Style arrays are assumed to be valid. + if (Array.isArray(cat.style)) return; -@param {layer} layer A json layer object. -@property {layer-style} layer.style The mapp-layer style configuration. -@property {Object} layer.cluster Cluster configuration for a point layer. -@property {feature-style} style.default Default feature style. -@property {feature-style} style.cluster Style for cluster feature. -@property {feature-style} style.selected Style for features of selected locations. -*/ -function clusterChecks(layer) { + // Ensure that the icon object is within a style object. + if (cat.icon) { + cat.style = { + icon: cat.icon + } + delete cat.icon + } - if (!layer.cluster) return + const defaultStyle = structuredClone(layer.style.default) - if (!layer.style.default.icon) { + if (cat.style.icon) { - layer.style.default = { - icon: { - type: 'dot' + // Do not merge default style into icon array. + if (Array.isArray(cat.style.icon)) return; + + // Do not merge default style into icon with type definition. + if (cat.style.icon.type) return; + + // Do not merge default style into svg [type] icons. + if (cat.style.icon.svg) return; + + if (defaultStyle.icon && !Array.isArray(defaultStyle.icon)) { + + cat.style.icon = { + ...defaultStyle.icon, + ...cat.style.icon } } - console.warn(`Cluster Layer: ${layer.key} has no default icon. 'Dot' will be assigned.`) + return; } - // Cluster layer must not have stroke or fill styles. - Object.keys(layer.style.default) - .filter(key => !['icon', 'scale'].includes(key)) - .forEach(key => { + // Create a mergeStyle from valid styleKeys + const mergeStyle = {} - console.warn(`Cluster Layer: ${layer.key}; ${key} key removed from default style.`) + Object.keys(defaultStyle) + .filter(key => styleKeys.has(key)) + .forEach(key => mergeStyle[key] = defaultStyle[key]) - delete layer.style.default[key] + cat.style = { + ...mergeStyle, + ...cat.style, + } + + Object.keys(cat) + .filter(key => styleKeys.has(key)) + .forEach(key => { + cat.style[key] = cat[key] + delete cat[key] }) - // Define default style cluster icon - layer.style.cluster = { - clusterScale: 1, - icon: { - type: 'dot' - }, - ...layer.style.cluster - } + iconObject(cat.style) - if (layer.style.cluster.zoomInScale) { + // Assign default icon if no cat style icon could be created. + if (!cat.style.icon && defaultStyle.icon) { - layer.style.zoomInScale = layer.style.cluster.zoomInScale + console.warn(`Layer:${layer.key}, Failed to evaluate icon: ${JSON.stringify(cat)}. Default icon will be assigned.`) - delete layer.style.cluster.zoomInScale + cat.style.icon = defaultStyle.icon } +} - if (layer.style.cluster.zoomOutScale) { +/** +@function iconObject - layer.style.zoomOutScale = layer.style.cluster.zoomOutScale +@description +The iconObject method parses a feature-style object without an icon object to check whether there are icon object specific properties defined in the style object. An icon object will be created from the icon specific properties with these properties being removed from the style object itself. - delete layer.style.cluster.zoomOutScale - } +@param {feature-style} style +@property {object} [style.icon] The style object already has an icon definition. +*/ +function iconObject(style) { + + // The style object already has an icon object. + if (style.icon) return; + + Object.keys(style) + .filter(key => iconKeys.has(key)) + .forEach(key => { + style.icon ??= {}; + style.icon[key] = style[key] + delete style[key] + }) } + +/** + @description + The handleHovers method parses the layer's hover/s properties. + It provides fallback values to the hover/s key, title properties as well as assign a featureHover method if not provided. + It also assigns the first hover string from the hovers if undefined. + @param {layer} layer The layer reference for the style/hover/s object. + @property {layer-style} layer.style The mapp-layer style configuration. + @property {string} [style.hover] The current hover applied to the feature styling. + @property {array} [style.hovers] An array of hover objects available to be applied as the layer.style.hover. + */ +function handleHovers(layer) { + // Handle multiple hovers in layer style. + if (layer.style?.hovers) { + + Object.keys(layer.style.hovers).forEach(key => { + + // required for the lookup of self referenced objects + layer.style.hovers[key].key = key + + // assign key as fallback title + layer.style.hovers[key].title ??= key; + }) + + // Assign the first key from hovers object as hover string property if undefined. + layer.style.hover ??= Object.keys(layer.style.hovers)[0] + + // Assign hover property from hovers object if string. + if (typeof layer.style.hover === 'string') { + + layer.style.hover = layer.style.hovers[layer.style.hover] + } + } + + // Set default featureHover method if not provided. + if (layer.style?.hover) { + layer.style.hover.method ??= mapp.layer.featureHover; + } +} \ No newline at end of file diff --git a/lib/location/decorate.mjs b/lib/location/decorate.mjs index 1a066956af..7347c9e468 100644 --- a/lib/location/decorate.mjs +++ b/lib/location/decorate.mjs @@ -91,7 +91,7 @@ function remove() { // Restore highlight interaction. // A different interaction may have been set from a location method. - this.layer.mapview.interaction.type !== 'highlight' + this.layer.mapview.interaction?.type !== 'highlight' && this.layer.mapview.interactions.highlight() // Reload the layer if possible. @@ -195,6 +195,7 @@ async function update() { // Update entry.values with newValues. entry.value = entry.newValue; + // Remove newValue delete entry.newValue; @@ -227,8 +228,24 @@ async function update() { this.updateCallbacks?.forEach(fn => typeof fn === 'function' && fn(this)) } +/** +@function syncFields +@async + +@description +The syncFields method sends a parameterised query to the location_get query template. The fields parameter will be populated from the fields params argument. + +Values of the location [this] infoj entry matching the fields will be updated with values from the query response. + +@param {array} fields +*/ async function syncFields(fields) { + // fields must be an array + if (!Array.isArray(fields)) { + fields = [fields] + } + const response = await mapp.utils.xhr( `${this.layer.mapview.host}/api/query?` + mapp.utils.paramString({ @@ -243,13 +260,12 @@ async function syncFields(fields) { // Return if response is falsy or error. if (!response || response instanceof Error) { console.warn('No data returned from location_get request using ID:', this.id) - return - } - // Check if the response is an array. - else if (Array.isArray(response)) { + return; + + } else if (Array.isArray(response)) { console.warn(`Location response returned more than one record for Layer: ${this.layer.key}.`) console.log('Location Get Response:', response) - return + return; } this.infoj diff --git a/lib/location/get.mjs b/lib/location/get.mjs index f48f5aff24..3025e0911e 100644 --- a/lib/location/get.mjs +++ b/lib/location/get.mjs @@ -26,12 +26,12 @@ The getInfoj() method is awaited before the location is decorated. @param {object} location @param {object} list Object in which locations are stored as properties. -@property {layer} location.layer The layer to which the location belongs. +@property {layer} [location.layer] The layer to which the location belongs. @property {string} location.id The ID must be unique for the layer dataset. @property {Array} [layer.infoj] The infoj array from the layer, this is required to decorate the location. -@property {mapview} layer.mapview -@property {object} mapview.interaction The current [highlight] interaction. -@property {boolean} mapview.hooks Hooks are enabled for the location.layer.mapview. +@property {mapview} [layer.mapview] +@property {object} [mapview.interaction] The current [highlight] interaction. +@property {boolean} [mapview.hooks] Hooks are enabled for the location.layer.mapview. @returns {Promise} Decorated location */ const locations = new Set() @@ -126,13 +126,7 @@ export async function getInfoj(location) { && Object.values(location.layer.tables).find(table => !!table) // Request the location fields from layer json & id. - const response = await mapp.utils.xhr(`${mapp.host}/api/query?` + mapp.utils.paramString({ - template: location.getTemplate, - locale: location.locale, - layer: location.layer.Key || location.layer.key, - table: location.table, - id: location.id, - })); + const response = await getFeatureResponse(location); // Check if the response is empty. if (!response || response instanceof Error) { @@ -156,7 +150,71 @@ export async function getInfoj(location) { title: _entry.title || response[_entry.field + '_label'], value: response[_entry.field], location - })) + })); return location.infoj } + + +/** +@function getFeatureResponse +@async +@description +Retrieves a feature either from local cache or remote API based on location parameters + +```js +const location = { + layer: { + featureLocation: true, + features: [{properties: {id: '123'}, geometry: {...}}] + }, + id: '123', + getTemplate: 'default', + locale: 'en', + table: 'locations' +}; +const feature = await getFeatureResponse(location); +``` + +@param {Object} location The location object containing feature lookup parameters +@property {layer} location.layer Layer information containing features and metadata +@property {boolean} [layer.featureLocation] Flag indicating if features are cached locally +@property {Array} [layer.features] Array of cached feature objects if available locally; Required for layer.featureLocation +@property {string} location.id ID of the feature to retrieve +@property {string} location.getTemplate Template name for API query +@property {string} location.locale Locale setting for API query +@property {string} location.table Table name for API query +@property {string} layer.Key Primary layer key identifier +@property {string} layer.key Fallback layer key identifier + +@returns {Promise} A promise that resolves to either: +- An object containing the feature geometry and properties if found +- null if no matching feature is found locally +- API response if fetched remotely +*/ +async function getFeatureResponse(location) { + + // Check if features are already available locally + if (location.layer.featureLocation) { + + const feature = location.layer.features + .find(feature => feature.properties[location.layer.qID] === location.id); + + return feature ? { + geometry: feature.geometry, + ...feature.properties + } : null; + } + + // If not available locally, fetch from API + const queryParams = { + template: location.getTemplate, + locale: location.locale, + layer: location.layer.Key || location.layer.key, + table: location.table, + id: location.id, + }; + + const apiUrl = `${mapp.host}/api/query?${mapp.utils.paramString(queryParams)}`; + return await mapp.utils.xhr(apiUrl); +} \ No newline at end of file diff --git a/lib/mapp.mjs b/lib/mapp.mjs index 40ed639118..9456f1a42e 100644 --- a/lib/mapp.mjs +++ b/lib/mapp.mjs @@ -34,7 +34,7 @@ import plugins from './plugins/_plugins.mjs' hooks.parse(); const _ol = { - current: '10.2.1' + current: '10.3.1' } if (window.ol === undefined) { @@ -54,12 +54,12 @@ if (window.ol === undefined) { } } -self.mapp = { +globalThis.mapp = { ol: _ol, version: '4.13.0-alpha', - hash: '2e1f3b08ffcd0706a030c06a61dd0d15c673dfbc', + hash: 'e19adad70af7047ccebda80df4825e8e408c0d91', host: document.head?.dataset?.dir || '', diff --git a/lib/mapview/_mapview.mjs b/lib/mapview/_mapview.mjs index bd28ec177f..4ad035d536 100644 --- a/lib/mapview/_mapview.mjs +++ b/lib/mapview/_mapview.mjs @@ -18,6 +18,8 @@ import attribution from './attribution.mjs' import addLayer from './addLayer.mjs' +import removeLayer from './removeLayer.mjs' + import fitView from './fitView.mjs' import getBounds from './getBounds.mjs' @@ -68,6 +70,7 @@ export default function decorate(mapview) { Object.assign(mapview, { srid: mapview.locale.srid || '3857', addLayer, + removeLayer, changeEnd, fitView, geometry, @@ -94,43 +97,9 @@ export default function decorate(mapview) { } }) - mapview.view ??= mapview.locale.view - - // Get the initialZoom for the mapview. - const initialZoom = mapp.hooks?.current?.z - || mapview.view?.z - || mapview.locale.minZoom - || 0 - - // Get the initialCenter for the mapview. - const initialCenter = ol.proj.fromLonLat([ - parseFloat(mapp.hooks?.current?.lng - || mapview.view?.lng - || 0), - parseFloat(mapp.hooks?.current?.lat - || mapview.view?.lat - || 0), - ]) - - // WARN! - mapview.locale.bounds && console.warn('locale.bounds have been renamed to locale.extent') + viewConfig(mapview) - const north = parseFloat(mapview.locale.extent?.north || 90); - const south = parseFloat(mapview.locale.extent?.south || -90); - const east = parseFloat(mapview.locale.extent?.east || 180); - const west = parseFloat(mapview.locale.extent?.west || -180); - - if ((north - south) >= 0 && (east - west) >= 0) { - mapview.extent = ol.proj.transformExtent([west, south, east, north], 'EPSG:4326', `EPSG:${mapview.srid}`); - } else { - console.warn('Invalid extent. Ensure north >= south and east >= west. Global extent is assumed.'); - mapview.extent = ol.proj.transformExtent([-180, -90, 180, 90], 'EPSG:4326', `EPSG:${mapview.srid}`); - } - - // Assign ol controls for mapview from locale. - mapview.controls ??= mapview.locale.mapviewControls - ?.filter(control => Object.hasOwn(ol.control, control)) - .map(control => new ol.control[control]) || [] + mapviewControls(mapview) // Map mapview.Map = new ol.Map({ @@ -146,14 +115,7 @@ export default function decorate(mapview) { }), moveTolerance: 5, controls: mapview.controls, //[new ol.control.Zoom()] - view: new ol.View({ - projection: `EPSG:${mapview.srid}`, - zoom: initialZoom, - minZoom: mapview.locale.minZoom, - maxZoom: mapview.locale.maxZoom, - center: initialCenter, - extent: mapview.extent - }) + view: new ol.View(mapview.view) }) // Observe whether the mapview.Map target element changes size. @@ -170,71 +132,7 @@ export default function decorate(mapview) { }).observe(mapview.Map.getTargetElement()) - // WARN! - mapview.locale.maskBounds - && console.warn('locale.maskBounds is set as mask:true in locale.extent') - - // Extent mask - if (mapview.locale.extent?.mask) { - - // Set world extent - const world = [ - [180, 90], - [180, -90], - [-180, -90], - [-180, 90], - [180, 90], - ] - - // Set locale extent - const extent = [ - [mapview.locale.extent.east, mapview.locale.extent.north], - [mapview.locale.extent.east, mapview.locale.extent.south], - [mapview.locale.extent.west, mapview.locale.extent.south], - [mapview.locale.extent.west, mapview.locale.extent.north], - [mapview.locale.extent.east, mapview.locale.extent.north], - ] - - // Create a maskFeature from the word and locale extent. - const maskFeature = new ol.Feature({ - geometry: new ol.geom - - // Use world and locale extent as polygon rings. - // The second ring will be subtracted. - .Polygon([world, extent]) - .transform(`EPSG:4326`, `EPSG:${mapview.srid}`) - }) - - // Add masklayer with the maskFeature - // and infinite zIndex to mapview.Map - mapview.Map.addLayer(new ol.layer.Vector({ - source: new ol.source.Vector({ - features: [maskFeature] - }), - style: { - 'fill-color': '#0004' - }, - zIndex: Infinity - })) - } - - // Check on old configuration of showScaleBar, scalebar and set to ScaleLine, and warn. - if (mapview.locale.showScaleBar) { - console.warn('locale.showScaleBar is deprecated. Use locale.ScaleLine.metric or locale.ScaleLine.imperial instead.') - // showScaleBar is set to true - so use metric default. - mapview.locale.ScaleLine = 'metric' - } - - // scalebar - if (mapview.locale.scalebar) { - console.warn('locale.scalebar is deprecated. Use locale.ScaleLine.metric or locale.ScaleLine.imperial instead.') - mapview.locale.ScaleLine = mapview.locale.scalebar - } - - // ScaleLine - mapview.locale.ScaleLine && mapview.Map.addControl(new ol.control.ScaleLine({ - units: mapview.locale.ScaleLine === 'imperial' ? 'imperial' : 'metric', - })) + viewMask(mapview) // Create mapview attribution. attribution(mapview) @@ -289,6 +187,183 @@ export default function decorate(mapview) { return mapview } +/** +@function viewConfig + +@description +The method sets the view config for the mapview. + +@param {object} mapview JSON params for a new mapview. +@property {object} [mapview.view] The view config. +@property {float} [view.lng = 0] Longitude +@property {float} [view.lat = 0] Latitude +*/ +function viewConfig(mapview) { + + mapview.view ??= { + } + + mapview.view.projection = `EPSG:${mapview.srid}` + + mapview.view.minZoom = mapview.locale.minZoom; + + mapview.view.maxZoom = mapview.locale.maxZoom; + + mapview.view.zoom = mapp.hooks?.current?.z + || mapview.view.z + || mapview.view.minZoom + || 0 + + mapview.view.center = ol.proj.fromLonLat([ + parseFloat(mapp.hooks?.current?.lng || mapview.view.lng || 0), + parseFloat(mapp.hooks?.current?.lat || mapview.view.lat || 0), + ]) + + viewExtent(mapview) +} + +/** +@function viewExtent + +@description +The methods checks the locale extent configuration and sets the view extent. + +@param {object} mapview JSON params for a new mapview. +@property {locale} mapview.locale The mapview locale. +@property {object} [mapview.view] The view config. +@property {object} [locale.extent] The locale/view extent. +*/ +function viewExtent(mapview) { + + if (mapview.locale.bounds) { + + console.warn('locale.bounds have been renamed to locale.extent') + mapview.locale.extent = mapview.locale.bounds + } + + const north = parseFloat(mapview.locale.extent?.north || 90); + const south = parseFloat(mapview.locale.extent?.south || -90); + const east = parseFloat(mapview.locale.extent?.east || 180); + const west = parseFloat(mapview.locale.extent?.west || -180); + + if ((north - south) >= 0 && (east - west) >= 0) { + + mapview.view.extent = ol.proj.transformExtent([west, south, east, north], 'EPSG:4326', `EPSG:${mapview.srid}`); + + } else { + + console.warn('Invalid extent. Ensure north >= south and east >= west. Global extent is assumed.'); + mapview.view.extent = ol.proj.transformExtent([-180, -90, 180, 90], 'EPSG:4326', `EPSG:${mapview.srid}`); + } +} + +/** +@function mapviewControls + +@description +The method parses mapview control configs in the locale and creates the mapview.controls array of openlayer map control elements. + +An array of mapviewControls, eg. `mapviewControls = ['Zoom']` can be defined to add the control to the mapview.Map on creation. + +@param {object} mapview JSON params for the new mapview.Map. +@property {locale} mapview.locale The mapview locale. +@property {array} [locale.mapviewControls] Array of openlayers map controls eg. 'Zoom'. +@property {boolean} [locale.showScaleBar] Legacy config to show metric ScaleLine. +@property {string} [locale.scalebar] Legacy config to define ScaleLine units. +@property {string} [locale.ScaleLine] String configuration for openlayers ScaleLine units ['metric' or 'imperial']. +*/ +function mapviewControls(mapview) { + + // Assign ol controls for mapview from locale. + mapview.controls = mapview.locale.mapviewControls + ?.filter(control => Object.hasOwn(ol.control, control)) + .map(control => new ol.control[control]) || [] + + if (mapview.locale.showScaleBar) { + + // The legacy boolean showScaleBar config will set the ScaleLine to metric. + mapview.locale.ScaleLine ??= 'metric' + } + + if (mapview.locale.scalebar) { + + // The legacy scalebar config will set as ScaleLine. + mapview.locale.ScaleLine ??= mapview.locale.scalebar + } + + if (mapview.locale.ScaleLine) { + + // The ScaleLine property must be imperial or metric + mapview.locale.ScaleLine = mapview.locale.ScaleLine === 'imperial' ? 'imperial' : 'metric'; + + mapview.controls.push(new ol.control.ScaleLine({ + units: mapview.locale.ScaleLine + })) + } +} + +/** +@function viewMask + +@description +A mask layer will be added to the mapview map if the mask flag is set in the locale.extent. + +@param {object} mapview JSON params for a new mapview. +@property {locale} mapview.locale The mapview locale. +@property {object} [locale.extent] The locale/view extent. +@property {boolean} [extent.mask] Apply mask layer to the view extent. +*/ +function viewMask(mapview) { + + if (mapview.locale.maskBounds) { + + console.warn('locale.maskBounds is set as mask:true in locale.extent') + } + + // Extent mask + if (!mapview.locale.extent?.mask) return; + + // Set world extent + const world = [ + [180, 90], + [180, -90], + [-180, -90], + [-180, 90], + [180, 90], + ] + + // Set locale extent + const extent = [ + [mapview.locale.extent.east, mapview.locale.extent.north], + [mapview.locale.extent.east, mapview.locale.extent.south], + [mapview.locale.extent.west, mapview.locale.extent.south], + [mapview.locale.extent.west, mapview.locale.extent.north], + [mapview.locale.extent.east, mapview.locale.extent.north], + ] + + // Create a maskFeature from the word and locale extent. + const maskFeature = new ol.Feature({ + geometry: new ol.geom + + // Use world and locale extent as polygon rings. + // The second ring will be subtracted. + .Polygon([world, extent]) + .transform(`EPSG:4326`, `EPSG:${mapview.srid}`) + }) + + // Add masklayer with the maskFeature + // and infinite zIndex to mapview.Map + mapview.Map.addLayer(new ol.layer.Vector({ + source: new ol.source.Vector({ + features: [maskFeature] + }), + style: { + 'fill-color': '#0004' + }, + zIndex: Infinity + })) +} + /** @function changeEnd @@ -338,7 +413,10 @@ async function mapviewPromise(mapview) { await mapp.utils.svgTemplates(mapview.locale.svgTemplates) - await mapp.utils.loadPlugins(mapview.locale.plugins); + if (mapview.loadPlugins) { + + await mapp.utils.loadPlugins(mapview.locale.plugins, Array.isArray(mapview.loadPlugins) ? mapview.loadPlugins : undefined); + } mapview.locale.syncPlugins ??= []; diff --git a/lib/mapview/addLayer.mjs b/lib/mapview/addLayer.mjs index 0fff7c22f8..b4f01c54a1 100644 --- a/lib/mapview/addLayer.mjs +++ b/lib/mapview/addLayer.mjs @@ -51,7 +51,7 @@ export default async function addLayer(layers) { // The layer.err is an array of errors from failing to retrieve templates asscoiated with a layer. layer.err && console.error(layer.err) - + // A default zIndex is assigned from the loop index to ensure layers are drawn in the order without an implicit zIndex. layer.zIndex ??= i diff --git a/lib/mapview/removeLayer.mjs b/lib/mapview/removeLayer.mjs new file mode 100644 index 0000000000..25da7b80f0 --- /dev/null +++ b/lib/mapview/removeLayer.mjs @@ -0,0 +1,61 @@ +/** +## /mapview/removeLayer +The module exports the removeLayer method which is bound to the mapview. + +@module /mapview/removeLayer +*/ + +/** +@function removeLayer +@description +A single layer key/object or an array of layer keys/objects can be removed from the mapview with the removeLayer method. +The layer will be hidden from the map if currently displayed. +The layer will be removed from the mapview.layers{} object. +The layer's OpenLayers instance will be disposed properly to prevent memory leaks. + +@param {(string|object|array)} layers A single layer key/object or an array of layer keys/objects to be removed from the mapview. +@returns {array} The array of removed layer keys is returned. +*/ +export default function removeLayer(layers) { + + // Handle single layer input + if (!Array.isArray(layers)) { + layers = [layers]; + } + + const removedLayers = []; + + for (const layer of layers) { + + // Convert layer object to key if needed + const layerKey = typeof layer === 'string' ? layer : layer.key; + + if (!layerKey || !this.layers[layerKey]) { + console.warn(`Layer ${layerKey} not found in mapview`); + continue; + } + + const targetLayer = this.layers[layerKey]; + + // Hide the layer if it's currently displayed + if (targetLayer.display) { + targetLayer.hide(); + } + + // Clean up OpenLayers layer instance if it exists + if (targetLayer.L) { + targetLayer.L.dispose(); + } + + // Remove layer from mapview.layers + delete this.layers[layerKey]; + removedLayers.push(layerKey); + } + + // Will resolve once the map has completed render + this.renderComplete = new Promise(resolve => { + this.Map.once('rendercomplete', resolve); + }); + + return removedLayers; +} diff --git a/lib/plugins/_plugins.mjs b/lib/plugins/_plugins.mjs index ba8dba26e2..e6faf22548 100644 --- a/lib/plugins/_plugins.mjs +++ b/lib/plugins/_plugins.mjs @@ -11,7 +11,7 @@ import { keyvalue_dictionary } from './keyvalue_dictionary.mjs' import { locator } from './locator.mjs' import { login } from './login.mjs' import { svg_templates } from './svg_templates.mjs' -import { userIDB } from './userIDB.mjs' +import { userLocale } from './userLocale.mjs' import { zoomBtn } from './zoomBtn.mjs' import { zoomToArea } from './zoomToArea.mjs' import { link_button } from './link_button.mjs' @@ -37,7 +37,7 @@ const plugins = { locator, login, svg_templates, - userIDB, + userLocale, zoomBtn, zoomToArea, link_button diff --git a/lib/plugins/userIDB.mjs b/lib/plugins/userIDB.mjs deleted file mode 100644 index 166a1e9197..0000000000 --- a/lib/plugins/userIDB.mjs +++ /dev/null @@ -1,59 +0,0 @@ -export function userIDB(plugin, mapview) { - - if (!mapp.user?.email) { - - console.warn(`The userIDB plugin requires a mapp.user`) - return; - } - - // Find the btnColumn element. - const btnColumn = document.getElementById('mapButton'); - - if (!btnColumn) return; - - plugin.title ??= 'Update userIDB locale' - - // Append the plugin btn to the btnColumn. - btnColumn.append(mapp.utils.html.node` - + ` + + // Append the plugin btn to the btnColumn. + layersNode.append(plugin.panel); +} diff --git a/lib/plugins/zoomBtn.mjs b/lib/plugins/zoomBtn.mjs index fe70c115c6..229c181cc2 100644 --- a/lib/plugins/zoomBtn.mjs +++ b/lib/plugins/zoomBtn.mjs @@ -25,6 +25,9 @@ Adds zoom in and zoom out buttons to the mapview. */ export function zoomBtn(plugin, mapview) { + // Plugin will err if called from layer. + if (!mapview) return; + const btnColumn = document.getElementById('mapButton'); // the btnColumn element only exist in the default mapp view. diff --git a/lib/ui.mjs b/lib/ui.mjs index 89b812a4c4..f4a1ba1c0e 100644 --- a/lib/ui.mjs +++ b/lib/ui.mjs @@ -29,7 +29,7 @@ import utils from './ui/utils/_utils.mjs' import Gazetteer from './ui/Gazetteer.mjs' -self.ui = { +const ui = { layers, locations, elements, @@ -37,9 +37,10 @@ self.ui = { Gazetteer, Dataview, Tabview, -} +}; -if (mapp) { +globalThis.ui = ui +if (mapp) { mapp.ui = ui } \ No newline at end of file diff --git a/lib/ui/Dataview.mjs b/lib/ui/Dataview.mjs index 94aa6bfc23..47b876e61d 100644 --- a/lib/ui/Dataview.mjs +++ b/lib/ui/Dataview.mjs @@ -106,22 +106,6 @@ export default async function Dataview(_this) { _this.hide ??= hide - // Create checkbox if a label is provided. - _this.chkbox = _this.label && mapp.ui.elements.chkbox({ - data_id: _this.key, - label: _this.label, - checked: !!_this.display, - disabled: _this.disabled, - onchange: (checked) => { - - _this.display = checked - - _this.display - ? _this.show() - : _this.hide() - } - }) - // Create dataview toolbar dataviewToolbar(_this) @@ -169,6 +153,9 @@ function show() { this.update instanceof Function && this.update() } + //Show toolbar buttons if there are any + this.btnRow?.style.setProperty('display','block') + this.target.style.display = 'block' } @@ -186,6 +173,9 @@ function hide() { this.display = false this.target.style.display = 'none' + + //Hide toolbar buttons if there are any + this.btnRow?.style.setProperty('display','none') } /** @@ -331,10 +321,16 @@ function dataviewToolbar(_this) { .map((key) => mapp.ui.utils[_this.dataview]?.toolbar[key]?.(_this)) .filter((item) => !!item); + // Create an element for the toolbar buttons + _this.btnRow = mapp.utils.html.node`
${toolbarElements}
` + + // By default btnRow is hidden + _this.btnRow.style.setProperty('display','none'); + // The panel will be assigned in a tabview. _this.panel = _this.target.appendChild(mapp.utils.html.node`
-
${toolbarElements}
+ ${_this.btnRow} ${target}`); // Assign dataview target as target. diff --git a/lib/ui/Tabview.mjs b/lib/ui/Tabview.mjs index def9e4ac87..38cf0905c2 100644 --- a/lib/ui/Tabview.mjs +++ b/lib/ui/Tabview.mjs @@ -73,54 +73,29 @@ The decorator method will add `show()` and `hide()` methods for the tab. */ function addTab(entry) { - // The entry already has a tab. - if (entry.tab) return; + // The entry already has a tab, and is not flagged as dynamic. + if (entry.tab && !entry.dynamic) return; const tabview = this - entry.activate ??= function(){ - - if (entry.create === undefined) { - - entry.create ??= function () { - mapp.ui.utils[entry.dataview]?.create(entry); - } - - entry.create() - - } else if (entry.dynamic) { - - entry.create() - } - - if (entry.update instanceof Function) { - - if (!entry.data || entry.activateUpdate) { - - // Call dataview update method if data is falsy or activateUpdate flag is set. - entry.update() - } - } - } + entry.activate ??= activateTab if (entry.location) { - + // The tabview should be removed if the location is removed. - entry.location.removeCallbacks.push(()=>entry.remove()) + entry.location.removeCallbacks.push(() => entry.remove()) - } - - else if (entry.layer) { + } else if (entry.layer) { // Show tab when layer is displayed. - entry.layer.showCallbacks.push(()=>{ + entry.layer.showCallbacks.push(() => { // Entry must have display flag. entry.display && entry.show() }) - + // Hide tab when layer is hidden. - entry.layer.hideCallbacks.push(()=>{ + entry.layer.hideCallbacks.push(() => { entry.remove() }) } @@ -134,7 +109,7 @@ function addTab(entry) { class="header" style="${entry.tab_style || ''}" onclick=${showTab}>${entry.label}` - + entry.panel ??= entry.target || mapp.utils.html.node`
` @@ -145,6 +120,42 @@ function addTab(entry) { // Must override dataview hide method. entry.hide = removeTab + /** + @function activateTab + + @description + The activateTab method is debounced for tabs being shown/added to a tabview. A tab may be a dataview object which requires to be created/updated within the context of the tab. + + The dataview may have an associated toolbar [btnRow element] which must be displayed. + */ + function activateTab() { + + if (entry.create === undefined) { + + entry.create ??= function () { + mapp.ui.utils[entry.dataview]?.create(entry); + } + + entry.create() + + } else if (entry.dynamic) { + + entry.create() + } + + if (entry.update instanceof Function) { + + if (!entry.data || entry.activateUpdate) { + + // Call dataview update method if data is falsy or activateUpdate flag is set. + entry.update() + } + } + + //Show toolbar buttons if there are any + entry.btnRow?.style.setProperty('display', 'block') + } + /** @function showTab @@ -171,12 +182,12 @@ function addTab(entry) { // This prevents each tab to activate when multiple tabs are added in quick succession. tabview.timer && window.clearTimeout(tabview.timer) - tabview.timer = window.setTimeout(entry.activate, 500) + tabview.timer = window.setTimeout(()=>entry.activate(), 500) if (tabview.showTab instanceof Function) { // Execute tabview method to show a tab. - tabview.showTab(entry) + tabview.showTab() } } diff --git a/lib/ui/layers/listview.mjs b/lib/ui/layers/listview.mjs index 1e69529ff5..b8a28d3d4f 100644 --- a/lib/ui/layers/listview.mjs +++ b/lib/ui/layers/listview.mjs @@ -1,139 +1,165 @@ /** -## mapp.ui.layers.listview +## /ui/layers/listview -The ui/layers/filters module exports the listview method to mapp.ui.layers{}. +The module exports the default listview method. -Dictionary entries: -- layer_group_hide_layers - -@requires /dictionary +@requires /mapp/ui/layers/view @module /ui/layers/listview */ -export default function (params) { - - if (!params.mapview) return - - if (!params.target) return - - const listview = { - node: params.target, - groups: {} - } - - // Loop through the layers and add to layers list. - Object.values(params.mapview.layers).forEach(layer => add(layer)) - +/** +@function listview - /** - * @function add - * @description - * Adds a layer to the layers list. - * @param {Object} layer The layer object. - * @returns {void} - */ +@description +Creates a listview for organizing and displaying map layers, optionally grouped. - // Loop through the layers and add to layers list. - function add(layer) { +A HTMLelement target property must be provided for the listview element to be rendered into. - // Do not create a layer view. - if (layer.hidden) return; +An initial list of decorated mapview layers is optional since layers can be added to the listview object through the add method. - // Create the layer view. - mapp.ui.layers.view(layer) +The listview method is a decorator which returns the decorated listview object. - if (!layer.group) { - listview.node.appendChild(layer.view) - listview.node.dispatchEvent(new CustomEvent('addLayerView', { - detail: layer - })) - return - } +```js +mapp.ui.layers.listview({ + layers: mapview.layers, + target: document.getElementById('layer-list') +}); +``` - // Create new layer group if group does not exist yet. - if (!listview.groups[layer.group]) createGroup(layer) +@param {Object} params - Configuration parameters +@property {HTMLElement} params.target DOM element where the listview will be rendered +@property {Object} [params.layers] - Map of layer objects to be added to the listview +*/ +export default function listview(params) { - // Add layer to group. - listview.groups[layer.group].addLayer(layer) + if (!params.target) return; - listview.node.dispatchEvent(new CustomEvent('addLayerView', { - detail: layer - })) + const listview = { + node: params.target, + groups: {}, + add, + createGroup } - /** - * @function createGroup - * @description - * Creates a group object and assigns the layer group to the listview object. - * @param {Object} layer The layer object. - * @returns {void} - */ - function createGroup(layer) { - - // Create group object. - const group = { - list: [] - } - - // Assign layer group to listview object. - listview.groups[layer.group] = group + if (typeof params.layers === 'object' && Object.values(params.layers).length) { - // Create hide all group layers button. - const hideLayers = mapp.utils.html.node` - ` + + return button; +} diff --git a/lib/utils/_utils.mjs b/lib/utils/_utils.mjs index a135c3fb1f..03081ae7b3 100644 --- a/lib/utils/_utils.mjs +++ b/lib/utils/_utils.mjs @@ -28,6 +28,8 @@ import { dataURLtoBlob } from './dataURLtoBlob.mjs' import { default as hexa } from './hexa.mjs' +import jsonParser from './jsonParser.mjs' + import loadPlugins from './loadPlugins.mjs' import getCurrentPosition from './getCurrentPosition.mjs' @@ -52,8 +54,12 @@ import * as svgSymbols from './svgSymbols.mjs' import svgTemplates from './svgTemplates.mjs' +import textFile from './textFile.mjs' + import * as userIndexedDB from './userIndexedDB.mjs' +import * as userLocale from './userLocale.mjs' + import * as gazetteer from './gazetteer.mjs' import { default as verticeGeoms } from './verticeGeoms.mjs' @@ -64,6 +70,8 @@ import { formatNumericValue, unformatStringValue } from './numericFormatter.mjs' import { versionCheck } from './versionCheck.mjs'; +import { temporal } from './temporal.mjs'; + export default { stats, render, @@ -80,6 +88,7 @@ export default { gazetteer, getCurrentPosition, hexa, + jsonParser, loadPlugins, merge, mobile, @@ -91,7 +100,10 @@ export default { style, svgSymbols, svgTemplates, + temporal, + textFile, userIndexedDB, + userLocale, verticeGeoms, xhr, versionCheck diff --git a/lib/utils/csvDownload.mjs b/lib/utils/csvDownload.mjs index 137dc84382..9e4c6a824d 100644 --- a/lib/utils/csvDownload.mjs +++ b/lib/utils/csvDownload.mjs @@ -58,7 +58,7 @@ function fieldsFunction(record, fields) { // Escape quotes in string value. if (typeof record[field.field] === 'string' && field.string) { - return `"${record[field.field].replaceAll('"', '\\"')}"` + return `"${record[field.field].replaceAll('"', '""')}"` } // Format number toLocaleString diff --git a/lib/utils/jsonParser.mjs b/lib/utils/jsonParser.mjs new file mode 100644 index 0000000000..2978d541ba --- /dev/null +++ b/lib/utils/jsonParser.mjs @@ -0,0 +1,193 @@ +/** +### /utils/jsonParser + +The jsonParser utility is used to parse a locale to be stored as a userLocale. + +@module /utils/jsonParser +*/ + +export default jsonParser; + +let sourceObjArray, targetObjArray; + +/** +@function jsonParser + +@description +The jsonParser returns an object which can be stored in an object store. + +The jsonParser method resets the arrays to hold parsed objects and removes the parser flags from all object in these arrays before returning the jsonObject with parsed properties from the obj param. + +@param {Object} obj A decorated object. +@returns {Object} Returns a JSON object which can be stored in an object store. +*/ +function jsonParser(obj) { + if (typeof obj !== 'object') return; + + const jsonObject = {}; + + sourceObjArray = []; + targetObjArray = []; + + propertyParser(jsonObject, obj); + + // Remove parser flags. + sourceObjArray.forEach((obj) => delete obj.__parsed); + targetObjArray.forEach((obj) => delete obj.__parsed); + + return jsonObject; +} + +/** +@function propertyParser + +@description +The propertyParser parses a source object and assigns properties which can be stored in an object store to the target object. + +@param {Object} target +@param {Object} source +*/ +function propertyParser(target, source) { + if (!isObject(source)) return source; + + // The source object must only be parsed once. + source.__parsed = true; + + sourceObjArray.push(source); + + targetObjArray.push(target); + + // Iterate through the source own enumerable string-keyed property key-value pairs. + for (const [key, value] of Object.entries(source)) { + // The ignoreKeys contain checks against prototype pollution. + if (new Set(['__proto__', 'constructor', 'mapview']).has(key)) { + continue; + } + + if (excludeProperty(value)) continue; + + if (assignValue(target, key, value)) continue; + + if (Array.isArray(value)) { + target[key] = value.map((entry) => + isObject(entry) ? propertyParser({}, entry) : entry + ); + continue; + } + + if (value.hasOwnProperty('__parsed')) { + if (value.key) { + target[key] = value.key; + } + continue; + } + + // Value must be an object at this point. + target[key] ??= {}; + + // Call recursive merge for target key object. + propertyParser(target[key], value); + } + + return target; +} + +/** +@function excludeProperty +@description +The excludeProperty method returns true if a property should be exluded from the target in the propertyParser iteration. + +@param {*} value +@returns {Boolean} True if the property should be excluded in regards to the property value. +*/ +function excludeProperty(value) { + + if (value instanceof Object) { + const constructor = Object.getPrototypeOf(value).constructor; + + if (!(constructor === Object || constructor === Array)) { + // Constructor may be from a class, function, html element + return true; + } + } + + if (Array.isArray(value)) { + + const firstItem = value[0] + + if (firstItem instanceof Object) { + + const constructor = Object.getPrototypeOf(firstItem).constructor; + + if (!(constructor === Object || constructor === Array)) { + // Constructor may be from a class, function, html element + return true; + } + + if (firstItem.__parsed) { + + return true + } + } + } +} + +/** +@function assignValue +@description +Returns true if a true, false, null, string, or num value can be assigned to the target object. + +Returns true if undefined value is skipped. + +@param {Object} target +@param {string} key +@param {*} value +@returns {Boolean} True if the value has been assigned. +*/ +function assignValue(target, key, value) { + // Prevent prototype polluting assignment. + if (key === '__proto__' || key === 'constructor') return true; + + if (value === undefined) { + return true; + } + + if (value === true) { + target[key] = true; + return true; + } + + if (value === false) { + target[key] = false; + return true; + } + + if (value === null) { + target[key] = null; + return true; + } + + if (typeof value !== 'object') { + target[key] = value; + return true; + } +} + +/** +@function isObject +Checks whether the item argument is an object but not true, false, null, or an array. + +@param {*} item +@returns {Boolean} True if the item is an object but not true, false, null, or an array. +*/ +function isObject(item) { + if (item === true) return false; + + if (item === false) return false; + + if (item === null) return false; + + if (Array.isArray(item)) return false; + + if (typeof item === 'object') return true; +} diff --git a/lib/utils/loadPlugins.mjs b/lib/utils/loadPlugins.mjs index 5c6188d012..a53d3388c3 100644 --- a/lib/utils/loadPlugins.mjs +++ b/lib/utils/loadPlugins.mjs @@ -22,7 +22,7 @@ The endsWith argument can be used to provide an array of string conditions on wh @returns {Promise} The promise returned from the method will resolve once all [plugin] import promises are settled. */ -export default function loadPlugins(plugins, endsWith = ['.js','.mjs']) { +export default function loadPlugins(plugins, endsWith = ['.js', '.mjs']) { if (!Array.isArray(plugins)) return; diff --git a/lib/utils/olScript.mjs b/lib/utils/olScript.mjs index ec98859344..3a24a94ebc 100644 --- a/lib/utils/olScript.mjs +++ b/lib/utils/olScript.mjs @@ -23,12 +23,12 @@ export default async function olScript() { script.type = 'application/javascript' - script.src = 'https://cdn.jsdelivr.net/npm/ol@v10.2.1/dist/ol.js' + script.src = 'https://cdn.jsdelivr.net/npm/ol@v10.3.1/dist/ol.js' script.onload = resolve document.head.append(script) - console.warn('Openlayers v10.2.1 loaded from script tag.') + console.warn('Openlayers v10.3.1 loaded from script tag.') }) } diff --git a/lib/utils/temporal.mjs b/lib/utils/temporal.mjs new file mode 100644 index 0000000000..3b75854628 --- /dev/null +++ b/lib/utils/temporal.mjs @@ -0,0 +1,96 @@ +/** +Utility module for transforming between Unix timestamp integers and formatted date strings. +Supports various date/time formats and localization options. +@module /utils/temporal +*/ +const DEFAULT_LOCALE = 'en-GB'; +const MILLISECONDS_IN_SECOND = 1000; + +/** +Safely converts Unix timestamp to JavaScript Date object +@private +@param {number} unixTimestamp Unix timestamp in seconds +@returns {Date} JavaScript Date object +@throws {Error} If timestamp is invalid +*/ +function toJSDate(unixTimestamp) { + if (!Number.isInteger(unixTimestamp)) { + throw new Error('Invalid timestamp: must be an integer'); + } + + return new Date(unixTimestamp * MILLISECONDS_IN_SECOND); +} + +/** +Formats a Unix timestamp into a localized date string. If not explicit in the param the language for the locale will be taken from the mapp.user object or fallback to the default format. +@param {Object} params Input parameters +@property {number} params.value Unix timestamp in seconds +@property {number} [params.newValue] Updated timestamp value (takes precedence over value) +@property {string} [params.locale=en-GB] Locale identifier +@property {Intl.DateTimeFormatOptions} [params.options] Formatting options +@returns {string} Formatted date string +@see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat|Intl.DateTimeFormat} +*/ +function dateString(params) { + params.locale ??= mapp.user?.language || DEFAULT_LOCALE; + + if (!params.value && !params.newValue) { + throw new Error('Either value or newValue must be provided'); + } + + const timestamp = params.newValue || params.value; + const date = toJSDate(timestamp); + + return new Intl.DateTimeFormat(params.locale, params.options).format(date); +} + +/** +Converts a date string to Unix timestamp. The current data will be used if no dateStr is provided. +@param {string} [dateStr] Date string parseable by JavaScript Date +@returns {number} Unix timestamp in seconds +@throws {Error} If date string is invalid +*/ +function dateToUnixEpoch(dateStr) { + const date = dateStr ? new Date(dateStr) : new Date(); + + if (isNaN(date.getTime())) { + throw new Error('Invalid date string provided'); + } + + return Math.floor(date.getTime() / MILLISECONDS_IN_SECOND); +} + +/** +Formats Unix timestamp to ISO datetime string (YYYY-MM-DDThh:mm:ss) +@param {Object} params Input parameters +@property {number} params.value Unix timestamp in seconds +@property {number} [params.newValue] Updated timestamp value +@returns {string} ISO datetime string without timezone +*/ +function datetime(params) { + const timestamp = params.newValue || params.value; + const date = toJSDate(timestamp); + + return date.toISOString().split('.')[0]; +} + +/** +Formats Unix timestamp to ISO date string (YYYY-MM-DD) +@param {Object} params Input parameters +@property {number} params.value Unix timestamp in seconds +@property {number} [params.newValue] Updated timestamp value +@returns {string} ISO date string +*/ +function date(params) { + const timestamp = params.newValue || params.value; + const date = toJSDate(timestamp); + + return date.toISOString().split('T')[0]; +} + +export const temporal = { + dateString, + dateToUnixEpoch, + datetime, + date +}; diff --git a/lib/utils/textFile.mjs b/lib/utils/textFile.mjs new file mode 100644 index 0000000000..c366256a0e --- /dev/null +++ b/lib/utils/textFile.mjs @@ -0,0 +1,32 @@ +/** +## /utils/textFile + +@module /utils/textFile +*/ + +/** +@function textFile + +@description TextFile Utils + +@param {Object} params +@property {string} params.text +*/ +export default function textFile(params) { + + if (typeof params.text !== 'string') return; + + params.filename ??= 'file.txt' + + params.type ??= 'text' + + const blob = new Blob([params.text], { + type: params.type, + }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `${params.filename}`; + a.click(); + URL.revokeObjectURL(url); +} diff --git a/lib/utils/userIndexedDB.mjs b/lib/utils/userIndexedDB.mjs index 345dbf9393..c1ca51743d 100644 --- a/lib/utils/userIndexedDB.mjs +++ b/lib/utils/userIndexedDB.mjs @@ -1,104 +1,24 @@ /** - * ### mapp.utils.userIndexedDB - * This [indexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Basic_Terminology) implementation allows to store, get, and update a locale object from the 'locales' object store in a user indexedDB. - * - * There are many different operations that the indexedDB can handle. Typically the operations are CRUD. - * - * The `userIndexDB` methods have all been moved to the `mapp.utils` object. - * - * The logic for initialisation for the userIndexedDB object is the following: - * - `userIndexedDB.open(store)` will open a DB with the following name `${mapp.user.email} - {mapp.user.title}. - * - The database will not be created if there is a pre-existing DB. - * - The creation will trigger the `onupgradeneeded` event which checks whether the request `store` exists in the userIndexedDB. - * - * The `process.env.TITLE` will be added to the user object in the cookie module. - * The `user.title` is required to generate a unique indexedDB for each user[email/instance[title]] - * - * All object stores use the key value as a keypath for object indicies. - * - * Adding the url parameter `useridb=true` will ask the default script to get the keyed locale from the user indexedDB. - * The userLocale will be assigned as locale if available. - * - * ```js - * if (mapp.hooks.current.useridb) { - - let userLocale = await mapp.utils.userIndexedDB.get('locales', locale.key) - - if (!userLocale) { - await mapp.utils.userIndexedDB.add('locales', locale) - } else { - locale = userLocale - } - } - * ``` - * - * The userIDB plugin adds a button to put [update] the locale in the user indexedDB. - * - * ```js - * export function userIDB(plugin, mapview) { - - // Find the btnColumn element. - const btnColumn = document.getElementById('mapButton'); - - if (!btnColumn) return; +### mapp.utils.userIndexedDB - // Append the plugin btn to the btnColumn. - btnColumn.append(mapp.utils.html.node` -