ui_layers_legends_utils.mjs

/**
## /ui/layers/legends/utils

The categorized theme legend module exports the categorizedTheme to the `ui.layers.legend{}` library object.

Dictionary entries:
  - layer_style_switch_caption
  - layer_style_switch_all
  - layer_style_cluster

@requires /dictionary

@module /ui/layers/legends/utils
*/

/**
@function createLegend
@description
Create legend entry for category within a theme.

@param {Object} cat The category object.
@param {Object} theme The theme configuration object.
@param {Object} layer The layer object.
**/
export function createLegend(cat, theme, layer) {
  cat.field ??= theme.field;

  // Check whether cat is in current filter.
  cat.disabled ??=
    layer.filter?.current[cat.field]?.ni?.indexOf(cat.value) >= 0;

  if (layer.featureFields && theme.distribution === 'count') {
    // Build a params object to pass to the numericFormatter.
    const params = {
      value: layer.featureFields[cat.field]?.[cat.value],
    };
    cat.count = mapp.utils.formatNumericValue(params);
    if (!cat.disabled && !cat.count) return;
  }

  const catLegendIcon = mapp.ui.elements.legendIcon({
    height: 24,
    width: 24,
    ...(cat._style || cat.style),
  });

  const icon = mapp.utils.html`<div
      style="height: 24px; width: 24px; grid-column: 1;">
      ${catLegendIcon}`;

  const classList = `label ${(theme.legend.switch && layer.filter && 'switch') || ''} ${
    (cat.disabled && 'disabled') || ''
  }`;

  const cat_label = cat.label + (cat.count ? ` [${cat.count}]` : '');

  // Cat label with filter function.
  const label = mapp.utils.html.node`<div
      class=${classList}
      style="grid-column: 2;
             overflow: hidden;
             text-overflow: ellipsis;
             text-wrap: nowrap;">
            ${cat_label}`;

  if (theme.legend.switch) label.onclick = (e) => catToggle(e, layer, cat);

  cat.node = mapp.utils.html.node`<div
      data-id=${cat.value}
      class="${theme.legend.alignContents}">
      ${icon}${label}`;

  // Push icon and label into legend grid.
  theme.legend.grid.push(cat.node);
}

/**
@function filterRemove

@description
Remove the cat value to the current filter.

@param {layer} layer The decorated mapp layer.
@param {object} cat The cat object from the theme.
*/
export function filterRemove(layer, cat) {
  if (layer.style.theme.filterOnly) return;

  if (Array.isArray(cat.keys)) {
    for (const cat of cat.keys) {
      // Splice key out of the NI array.
      layer.filter.current[cat.field].ni.splice(
        layer.filter.current[cat.field].ni.indexOf(key),
        1,
      );
    }
  } else {
    // Splice value out of the NI array.
    layer.filter.current[cat.field].ni.splice(
      layer.filter.current[cat.field].ni.indexOf(cat.value),
      1,
    );
  }

  // Delete current field filter if NI array is empty.
  if (!layer.filter.current[cat.field].ni.length) {
    delete layer.filter.current[cat.field].ni;
    if (!Object.keys(layer.filter.current[cat.field]).length) {
      delete layer.filter.current[cat.field];
    }
  }

  const filter = layer.filter.list?.find(
    (f) => f.type === 'ni' && f.field === cat.field,
  );

  if (filter?.card) {
    filter.card
      .querySelector('.filter')
      .replaceWith(mapp.ui.layers.filters.ni(layer, filter));
  }
}

/**
@function filterAdd

@description
Add the cat value to the current filter.

@param {layer} layer The decorated mapp layer.
@param {object} cat The cat object from the theme.
*/
export function filterAdd(layer, cat) {
  if (layer.style.theme.filterOnly) return;

  // Create empty field filter object if non exists.
  if (!layer.filter.current[cat.field]) {
    layer.filter.current[cat.field] = {};
  }

  // Create empty NI filter array for field if non exists.
  if (!layer.filter.current[cat.field].ni) {
    layer.filter.current[cat.field].ni = [];
  }

  // Push cat value into the NI filter array.
  layer.filter.current[cat.field].ni.push(cat.keys || cat.value);

  // Flatten the filter in case of arrays filter.
  layer.filter.current[cat.field].ni =
    layer.filter.current[cat.field].ni.flat();

  const filter = layer.filter.list?.find(
    (f) => f.type === 'ni' && f.field === cat.field,
  );

  if (filter?.card) {
    filter.card
      .querySelector('.filter')
      .replaceWith(mapp.ui.layers.filters.ni(layer, filter));
  }
}

/**
@function catToggle

@description
The method toggles the disabled class on the event target element.

If toggled [on] the filterAdd method will be called and the style will be set to null.

If toggled [off] the filterRemove method will be called and the style will be restored from the cat._style.

@param {Event} e The cat label click event.
@param {layer} layer The decorated mapp layer.
@param {object} cat The cat object from the theme.
*/
export function catToggle(e, layer, cat) {
  const toggle = e.target.classList.toggle('disabled');

  if (toggle) {
    cat.disabled = true;

    filterAdd(layer, cat);

    // Store style to toggle on.
    cat._style = cat.style;

    // The feature should not be rendered.
    cat.style = null;
  } else {
    delete cat.disabled;

    filterRemove(layer, cat);

    // If the filter is defined in the config for initial display of just a subset of catergories, _style may not yet exist.
    cat._style ??= cat.style;

    // Set the style to _style that is a stored style.
    cat.style = cat._style;

    delete cat._style;
  }

  layer.style.theme.filterOnly ? layer.L.changed() : layer.reload();
}

/**
@function distributedLegend
@description
This function hides the distribution count theme on load.
@param {layer} layer The layer the theme exists on.
@returns {HTMLElement} The legend node
**/
export function distributedLegend(layer) {
  const displayStyle = layer.display ? 'block' : 'none';

  const theme = layer.style.theme;

  //Hide distribution count themes on load.
  theme.legend.node.style.setProperty('display', displayStyle);

  //Set the legend display to match layer show.
  layer.style.legend?.style.setProperty('display', displayStyle);

  //Hide/Show meta tag.
  theme.meta_node?.style.setProperty('display', displayStyle);

  return theme.legend.node;
}

/**
@function clusterLegend
@description Adds a cluster icon for the theme.
@param layer The layer the theme exists on.
**/
export function clusterLegend(layer) {
  const theme = layer.style.theme;
  // Attach row for cluster locations.
  // Create cluster icon.
  const icon = mapp.utils.html`
      <div
        style="height: 40px; width: 40px;">
        ${mapp.ui.elements.legendIcon({
          height: 40,
          icon: layer.style.cluster.icon,
          width: 40,
        })}`;

  // Create cluster label.
  const label = mapp.utils.html`
      <div
        class="label">
        ${mapp.dictionary.layer_style_cluster}`;

  // Push icon and label into legend grid.
  theme.legend.grid.push(mapp.utils.html`<div
      data-id="cluster"
      class=${theme.legend.alignContents}>
      ${icon}${label}`);
}

/**
@function themeParser
@description Sets nullish assignment of values and classList properties on the theme.
@param layer The layer the theme exists on.
**/
export function themeParser(layer) {
  const theme = layer.style.theme;

  theme.filterOnly ??= layer.style.filterOnly;

  theme.legend ??= {};

  theme.legend.grid = [];

  // Make 'left' default alignment.
  theme.legend.alignContents ??= 'left';

  //Prevent multiple contents classes being added.
  if (!theme.legend.alignContents.includes('contents'))
    theme.legend.alignContents += ' contents';

  theme.legend.classList = `contents-wrapper ${
    theme.legend?.layout || 'grid'
  } ${theme.legend?.nowrap ? 'nowrap' : ''}`;

  // if nowrap is set, we need to allow overflow scroll in case of too many items
  theme.legend.style = theme.legend?.nowrap ? 'overflow: scroll;' : '';
}