/**
### /ui/elements/layerStyle
The module exports an object of methods to for the creation of layer style elements.
Dictionary entries:
- layer_style_display_hover
- layer_style_display_labels
- layer_style_opacity
- layer_style_select_theme
- icon_scaling_field
- icon_scaling_label
- icon_scaling_cluster
- icon_scaling_zoom_in
- icon_scaling_zoom_out
@requires /dictionary
@requires /ui/layers
@module /ui/elements/layerStyle
*/
const methods = {
hover,
hovers,
icon_scaling,
label,
labels,
opacitySlider,
panel,
theme,
themes,
};
export default methods;
/**
@function panel
@description
The panel() style element method creates an array of style.elements from all available style property keys if not implicit.
The elements array will be mapped to return a flat array of HTMLElements as content for the layer.style.view node.
The layer.style.view is returned for the style drawer shown in the default mapp view layer.view.
@param {layer} layer A decorated mapp layer with a style object.
@property {layer-style} layer.style The layer style configuration.
@property {Array} [style.elements] Array of method keys to order elements in the layer.style.view.
@property {HTMLElement} [style.legend] The legend node created by the theme element method.
@property {Array} layer.showCallbacks Array of methods executed on layer.show().
@returns {HTMLElement} The layer.style.view element.
*/
function panel(layer) {
if (!layer.style) return;
// The theme must be applied before the label and hover elements are created.
applyTheme(layer);
layer.style.elements ??= Object.keys(layer.style);
// The layer.style.elements define which elements should be added in which order to the layer.style.view element.
const content = [];
for (const key of layer.style.elements) {
// Array may be defined in layer json with keys not defined in the layer.style.
if (!Object.hasOwn(layer.style, key)) continue;
if (!Object.hasOwn(methods, key)) continue;
const element = methods[key](layer);
if (element) content.push(element);
}
if (!content.length) return;
// The theme element method creates the style.legend node into which the drawLegend method renders. The panel method may be called repeatedly, eg. when the theme is changed, but the stable drawLegend reference must only be registered once.
if (!layer.showCallbacks.includes(mapp.ui.layers.drawLegend)) {
layer.showCallbacks.push(mapp.ui.layers.drawLegend);
}
layer.style.view = mapp.utils.html.node`<div>${content}`;
return layer.style.view;
}
/**
@function applyTheme
@description
The applyTheme method assigns the label and hover configuration referenced by the setLabel and setHover keys of the current theme.
The method is called from the panel method prior to the creation of the style elements. The label and hover elements are therefore created for the configuration assigned by the current theme.
@param {layer} layer A decorated mapp layer with a style object.
@property {layer-style} layer.style The layer style configuration.
@property {Object} [style.theme] The current theme.
@property {string} [theme.setLabel] Key for label from style.labels{} to assign.
@property {string} [theme.setHover] Key for hover from style.hovers{} to assign.
*/
function applyTheme(layer) {
if (!layer.style.theme) return;
// Handle setLabel and labels in layer style.
if (Object.hasOwn(layer.style.labels ?? {}, layer.style.theme.setLabel)) {
layer.style.label = layer.style.labels[layer.style.theme.setLabel];
}
// Handle setHover and hovers in layer style.
if (Object.hasOwn(layer.style.hovers ?? {}, layer.style.theme.setHover)) {
layer.style.hover = layer.style.hovers[layer.style.theme.setHover];
// Assign default featureHover method if non is provided.
layer.style.hover.method ??= mapp.layer.featureHover;
}
}
/**
@function hover
@description
The hover() style element method will return a checkbox to toggle the display of the current hover.
@param {layer} layer A decorated mapp layer with a style object.
@property {layer-style} layer.style The layer style configuration.
@property {Object} style.hover The current hover.
@property {Boolean} hover.hidden Whether a checkbox should be returned.
@property {Boolean} hover.display The state of the hover.
@returns {HTMLElement} A checkbox element to toggle the current hover.
*/
function hover(layer) {
if (!layer.style.hover) return;
if (layer.style.hover.hidden) return;
return mapp.ui.elements.chkbox({
checked: !!layer.style.hover.display,
data_id: `hoverCheckbox-${layer.key}`,
label:
(layer.style.hovers && mapp.dictionary.layer_style_display_hover) ||
layer.style.hover.title ||
mapp.dictionary.layer_style_display_hover,
onchange: (checked) => {
layer.style.hover.display = checked;
},
});
}
/**
@function hovers
@description
The hovers() style element method will return a dropdown to change the current hover assigned to a layer.
@param {layer} layer A decorated mapp layer with a style object.
@property {layer-style} layer.style The layer style configuration.
@property {Object} style.hover The current hover.
@property {Object} style.hovers Object where each property represents a hover configuration.
@returns {HTMLElement} A dropdown element to switch the current hover.
*/
function hovers(layer) {
if (!layer.style.hover) return;
if (layer.style.hover.hidden) return;
if (!layer.style.hovers) return;
// It takes two 2 tango!
if (Object.keys(layer.style.hovers).length < 2) return;
const entries = Object.keys(layer.style.hovers)
// Check if that label is set to be hidden.
.filter((key) => !layer.style.hovers[key].hidden)
.map((key) => ({
option: key,
title: layer.style.hovers[key].title || key,
}));
return mapp.ui.elements.dropdown({
callback: (e, entry) => {
const display = layer.style.hover.display;
// Set hover from dropdown option.
layer.style.hover = layer.style.hovers[entry.option];
// Assign default featureHover method if non is provided.
layer.style.hover.method ??= mapp.layer.featureHover;
layer.style.hover.display = display;
},
data_id: `hoversDropdown-${layer.key}`,
entries,
placeholder: layer.style.hover.title,
});
}
/**
@function label
@description
The label() style element method will return a checkbox to toggle the display of the current label.
@param {layer} layer A decorated mapp layer with a style object.
@property {layer-style} layer.style The layer style configuration.
@property {Object} style.label The current label.
@property {Boolean} label.hidden Whether a checkbox should be returned.
@property {Boolean} label.display The state of the label.
@returns {HTMLElement} A checkbox element to toggle the current label.
*/
function label(layer) {
if (!layer.style.label) return;
if (layer.style.label.hidden) return;
if (layer.style.labelCheckbox) {
return layer.style.labelCheckbox;
}
// The title will only be shown in dropdown for multiple labels option.
let label = layer.style.labels && mapp.dictionary.layer_style_display_labels;
// Assign label from single label object title.
label ??=
layer.style.label.title || mapp.dictionary.layer_style_display_labels;
layer.style.labelCheckbox = mapp.ui.elements.chkbox({
checked: !!layer.style.label.display,
data_id: `labelCheckbox-${layer.key}`,
label,
onchange: (checked) => {
layer.style.label.display = checked;
layer.reload();
},
});
layer.changeEndCallbacks.push(zoomLabel);
return layer.style.labelCheckbox;
}
/**
@function zoomLabel
@description
The method checks whether the label object has a min or maxZoom restriction and will toggle the status of the associated checkbox.
@param {layer} layer A decorated mapp layer with a style object.
*/
function zoomLabel(layer) {
if (layer.style.label.minZoom || layer.style.label.maxZoom) {
const z = layer.mapview.Map.getView().getZoom();
if (z <= layer.style.label.minZoom) {
layer.style.labelCheckbox?.classList.add('disabled');
} else if (z >= layer.style.label.maxZoom) {
layer.style.labelCheckbox?.classList.add('disabled');
} else {
layer.style.labelCheckbox?.classList.remove('disabled');
}
}
}
/**
@function labels
@description
The labels() style element method will return a dropdown to change the current label assigned to a layer.
@param {layer} layer A decorated mapp layer with a style object.
@property {layer-style} layer.style The layer style configuration.
@property {Object} style.label The current label.
@property {Object} style.labels Object where each property represents a label configuration.
@returns {HTMLElement} A dropdown element to switch the current label.
*/
function labels(layer) {
if (!layer.style.label) return;
if (layer.style.label.hidden) return;
if (!layer.style.labels) return;
// It takes two 2 tango!
if (Object.keys(layer.style.labels).length < 2) return;
const entries = Object.keys(layer.style.labels)
.filter((key) => !layer.style.labels[key].hidden)
.map((key) => ({
option: key,
title: layer.style.labels[key].title || key,
}));
return mapp.utils.html`
<div>${mapp.dictionary.layer_style_select_label}</div>
${mapp.ui.elements.dropdown({
callback,
data_id: 'labelsDropdown',
entries,
placeholder: layer.style.label.title,
})}`;
function callback(e, entry) {
const display = layer.style.label.display;
// Set label from dropdown option.
layer.style.label = layer.style.labels[entry.option];
layer.style.label.display = display;
layer.reload();
// layer.reload does not trigger changeEndCallbacks.
zoomLabel(layer);
}
}
function opacitySlider(layer) {
return mapp.ui.elements.slider({
callback: (e) => {
layer.L.setOpacity(Number.parseFloat(e / 100));
},
data_id: 'opacitySlider',
label: `${mapp.dictionary.layer_style_opacity}`,
max: 100,
min: 0,
val: Number.parseInt(layer.L.getOpacity() * 100),
});
}
/**
@function theme
@description
The theme() style element method will returns a content array with elements for the theme meta text and legend.
@param {layer} layer A decorated mapp layer with a style object.
@property {layer-style} layer.style The layer style configuration.
@property {HTMLElement} [style.legend] The legend node created by the theme method into which the drawLegend method renders.
@property {Object} style.theme The current theme.
@property {string} [theme.meta] Meta text to display.
@property {HTMLElement} [theme.meta_node] The node created for the theme.meta text.
@returns {HTMLElement} <div> with contents array for the theme meta and legend.
*/
function theme(layer) {
if (!layer.style.theme) return;
const content = [];
if (layer.style.theme.meta) {
layer.style.theme.meta_node = mapp.utils.html
.node`<p>${layer.style.theme.meta}`;
content.push(layer.style.theme.meta_node);
}
// The legend methods replace the children of the layer.style.legend node with the legend content.
layer.style.legend = mapp.utils.html.node`<div class="legend">`;
content.push(layer.style.legend);
// The drawLegend method is registered once as a layer.showCallbacks method in the panel method. The legend must be drawn here for the new legend node, since the theme method may be called after the layer has been shown and neither show() nor reload() will run the showCallbacks.
mapp.ui.layers.drawLegend(layer);
return mapp.utils.html.node`<div data-id="layerTheme">${content}`;
}
/**
@function themes
@description
The themes() style element method will return a dropdown to change the current theme assigned to a layer.
The dropdown callback assigns the theme selected from the style.themes{} configuration and recreates the style panel for the current theme.
A location layer entry has no style drawer. The elements from the panel method replace the children of the entry style.panel. The children of a new style drawer replace those of the style drawer in the layer.view otherwise. The targets are exclusive since a node can only be moved into one parent.
@param {layer} layer A decorated mapp layer with a style object.
@property {layer-style} layer.style The layer style configuration.
@property {HTMLElement} [style.panel] The style panel element of a location layer entry.
@property {Object} style.theme The current theme.
@property {Object} style.themes Object where each property represents a theme.
@property {string} [theme.title] Theme title for the dropdown placeholder and entries.
@property {HTMLElement} [layer.view] The layer view containing the style drawer element.
@property {Function} layer.reload The layer reload method called after the theme has been set.
@returns {HTMLElement} A dropdown element to switch the current theme.
*/
function themes(layer) {
if (!layer.style.themes) return;
// It takes two 2 tango!
if (Object.keys(layer.style.themes).length < 2) return;
const entries = Object.keys(layer.style.themes).map((key) => ({
option: key,
title: layer.style.themes[key].title || key,
}));
function callback(e, entry) {
// Set theme from dropdown option.
layer.style.theme = layer.style.themes[entry.option];
// The style panel is recreated for the current theme. The panel method applies the theme and the theme method draws the legend.
if (layer.style.panel) {
// A location layer entry has no style drawer. The style elements replace the children of the entry style.panel.
const stylePanel = panel(layer);
stylePanel && layer.style.panel.replaceChildren(...stylePanel.children);
} else {
// The children of the new style drawer replace those of the style drawer in the layer.view.
const styleDrawer = mapp.ui.layers.panels.style(layer);
styleDrawer &&
layer.view
?.querySelector('[data-id=style-drawer]')
?.replaceChildren(...styleDrawer.children);
}
layer.reload();
}
const dropdown = mapp.utils.html`<div>
${mapp.dictionary.layer_style_select_theme}
${mapp.ui.elements.dropdown({
callback,
data_id: 'themesDropdown',
entries,
placeholder: layer.style.theme.title,
})}`;
return dropdown;
}
/**
@function icon_scaling
@description
The icon_scaling() style element method will returns a contents array with controls for icon.scaling properties (field, icon, clusterScale, zoomInScale, and zoomOutScale).
@param {layer} layer A decorated mapp layer with a style object.
@property {layer-style} layer.style The layer style configuration.
@property {Object} style.icon_scaling The icon_scaling config.
@property {Boolean} icon_scaling.hidden Do not show icon_scaling controls in panel.
@property {String} [icon_scaling.field] Assign field value as feature property for scaling point feature icons.
@property {Object} [icon_scaling.fields] An Object of objects with fields that can be assigned the feature property for scaling.
@property {Boolean} [icon_scaling.icon] Add control to alter the base icon scale.
@property {Boolean} [icon_scaling.clusterScale] Add control to alter the cluster.clusterScale value.
@property {Boolean} [icon_scaling.zoomInScale] Add control to alter the zoomInScale value.
@property {Boolean} [icon_scaling.zoomOutScale] Add control to alter the zoomOutScale value.
@returns {HTMLElement} <div> with contents array of HTMLElements to control icon scaling.
*/
function icon_scaling(layer) {
if (!layer.style.icon_scaling) return;
if (layer.style.icon_scaling.hidden) return;
const content = [];
if (layer.style.icon_scaling.fields) {
const entries = Object.keys(layer.style.icon_scaling.fields).map((key) => ({
option: layer.style.icon_scaling.fields[key].field,
title: layer.style.icon_scaling.fields[key].title || key,
key: key,
}));
//Push an entry with no option so scaling can be disabled
entries.push({ title: mapp.dictionary.icon_scaling_no_scaling });
layer.style.icon_scaling.placeholder ??=
mapp.dictionary.icon_scaling_select_one;
layer.style.icon_scaling.title ??= mapp.dictionary.icon_scaling_title;
const placeholder =
Object.keys(layer.style.icon_scaling.fields).find(
(key) =>
layer.style.icon_scaling.fields[key].field ===
layer.style.icon_scaling.field,
) || Object.keys(layer.style.icon_scaling.fields)[0];
content.push(
mapp.utils.html`
<h3>${layer.style.icon_scaling.title}`,
mapp.ui.elements.dropdown({
data_id: 'iconScalingFieldsDropdown',
entries: entries,
callback: (e, options) => {
if (options.option) {
Object.assign(layer.style.icon_scaling, {
...layer.style.icon_scaling.fields[options.key],
});
} else {
delete layer.style.icon_scaling.field;
}
//Reload layer to re-run featureStyle
layer.reload();
layer.L.changed();
},
placeholder:
layer.style.icon_scaling.fields[placeholder].title ||
placeholder ||
layer.style.icon_scaling.placeholder,
}),
);
} else if (layer.style.icon_scaling.field) {
content.push(
mapp.ui.elements.chkbox({
checked: !!layer.style.icon_scaling.field,
data_id: `iconScalingFieldCheckbox-${layer.key}`,
label: mapp.dictionary.icon_scaling_field,
onchange: (checked) => {
if (checked) {
layer.style.icon_scaling.field = layer.style.icon_scaling._field;
delete layer.style.icon_scaling._field;
} else {
layer.style.icon_scaling._field = layer.style.icon_scaling.field;
delete layer.style.icon_scaling.field;
}
layer.L.changed();
},
}),
);
}
if (layer.style.icon_scaling.icon) {
layer.style.default.scale ??= 1;
layer.style.icon_scaling.maxScale ??= layer.style.default.scale * 3;
layer.style.icon_scaling.minScale ??= 0.1;
layer.style.icon_scaling.icon_scaling_label ??=
mapp.dictionary.icon_scaling_label;
content.push(
mapp.ui.elements.slider({
callback: (val) => {
layer.style.default.scale = Number.parseFloat(val);
clearTimeout(layer.style.timeout);
layer.style.timeout = setTimeout(() => layer.L.changed(), 400);
},
data_id: 'iconScalingSlider',
label: layer.style.icon_scaling.icon_scaling_label,
max: layer.style.icon_scaling.maxScale,
min: layer.style.icon_scaling.minScale,
step: layer.style.default.scale / 10,
val: layer.style.default.scale,
}),
);
}
if (
layer.style.icon_scaling.clusterScale &&
layer.style.cluster?.clusterScale
) {
layer.style.cluster.clusterScale ??= 1;
content.push(
mapp.ui.elements.slider({
callback: (val) => {
layer.style.cluster.clusterScale = Number.parseFloat(val);
clearTimeout(layer.style.timeout);
layer.style.timeout = setTimeout(() => layer.L.changed(), 400);
},
data_id: 'iconScalingClusterSlider',
label: mapp.dictionary.icon_scaling_cluster,
max: layer.style.cluster.clusterScale * 3,
min: 0,
step: layer.style.cluster.clusterScale / 10,
val: layer.style.cluster.clusterScale,
}),
);
}
// Only add this elements if icon_scaling.zoomInScale is defined.
if (layer.style.icon_scaling.zoomInScale) {
// Assign zoomInScale as 1 if not implicit.
layer.style.zoomInScale ??= 1;
content.push(
mapp.ui.elements.slider({
callback: (val) => {
layer.style.zoomInScale = Number.parseFloat(val);
clearTimeout(layer.style.timeout);
layer.style.timeout = setTimeout(() => layer.L.changed(), 400);
},
data_id: 'iconScalingZoomInSlider',
label: mapp.dictionary.icon_scaling_zoom_in,
max: layer.style.zoomInScale * 3,
min: 0,
step: layer.style.zoomInScale / 10,
val: layer.style.zoomInScale,
}),
);
}
// Only add this elements if icon_scaling.zoomOutScale is defined.
if (layer.style.icon_scaling.zoomOutScale) {
// Assign zoomOutScale as 1 if not implicit.
layer.style.zoomOutScale ??= 1;
content.push(
mapp.ui.elements.slider({
callback: (val) => {
layer.style.zoomOutScale = Number.parseFloat(val);
clearTimeout(layer.style.timeout);
layer.style.timeout = setTimeout(() => layer.L.changed(), 400);
},
data_id: 'iconScalingZoomOutSlider',
label: mapp.dictionary.icon_scaling_zoom_out,
max: layer.style.zoomOutScale * 3,
min: 0,
step: layer.style.zoomOutScale / 10,
val: layer.style.zoomOutScale,
}),
);
}
return mapp.utils.html.node`<div>${content}`;
}