layer_featureStyle.mjs

/**
### map.layer.featureStlye()
This module exports a style function for a layer
@module /layer/featureStyle
*/

/**
Creates a style function for a layer.
@function featureStyle
@description
Returns a style function for OL vector layers.

@param {Object} layer The layer object.
@property {layer-style} layer.style The layer style config.
@property {Boolean} [style.cache] The feature style should be retrieved from the feature 'Styles' property.

@returns {function} The style function for the layer.
*/
export default function featureStyle(layer) {
  /**
  @function Style
  @description
  Style functions process [vector] features and return an OL style to the layer render.
  */

  return function Style(feature) {
    // Style caching must be enabled with flag.
    if (layer.style.cache === true) {
      // Check for and return existing Styles object.
      const Styles = feature.get('Styles');

      if (Styles) return Styles;
    }

    // Assign feature properties.
    featureProperties(feature);

    if (feature.properties === null) {
      // The feature will not be visible.
      return null;
    }

    // Set style.default as feature.style.
    feature.style = layer.style.default;

    if (Object.hasOwn(mapp.layer.themes, layer.style.theme?.type)) {
      // Apply theme style to style object.
      mapp.layer.themes[layer.style.theme?.type]?.(layer.style.theme, feature);
    }

    // The feature must not be displayed.
    if (feature.style === null) return;

    // Style cluster point features.
    clusterStyle(feature);

    // Assign highlight style if required.
    highlightStyle(feature);

    // Assign label style if required.
    labelStyle(feature);

    // Assign selected style if required.
    selectedStyle(feature);

    return mapp.utils.style(feature.style, feature);
  };

  /**
  @function featureProperties
  @description
  Assigns feature properties based on layer configuration.

  @param {Object} feature The feature object.
  */
  function featureProperties(feature) {
    feature.properties = feature.getProperties();

    if (
      layer.featureSet?.size &&
      !layer.featureSet.has(feature.properties.id)
    ) {
      feature.properties = null;
      return;
    }

    // Assign MVT feature properties from the layer featuresObject.
    if (layer.featuresObject) {
      if (Object.hasOwn(layer.featuresObject, feature.properties.id)) {
        Object.assign(
          feature.properties,
          layer.featuresObject[feature.properties.id],
        );
      } else {
        feature.properties = null;
        return;
      }
    }

    // Check whether feature is in lookup.
    if (Array.isArray(layer.featureLookup)) {
      // Find feature with matching ID property in the featureLookup
      const lookupFeature = layer.featureLookup.find(
        (f) => f[layer.featureLookupId || 'id'] === feature.properties.id,
      );

      // Do not style features not found in the lookup array.
      if (!lookupFeature) {
        feature.properties = null;
        return;
      }

      // Assign feature.properties from the lookupFeature for subsequent styling
      Object.assign(feature.properties, lookupFeature);
    }

    // Geojson / WKT features may have a properties property
    if (feature.properties?.properties) {
      // This shouldn't happen anymore.
      console.warn(`Feature with properties.properties`);
      Object.assign(feature.properties, feature.properties.properties);
      delete feature.properties.properties;
    }
  }

  /**
  @function clusterStyle
  @description
  Applies cluster style to the feature based on layer configuration.

  @param {Object} feature - The feature object.
  */
  function clusterStyle(feature) {
    if (!feature.properties?.count) return;

    if (feature.properties.count === 1) return;

    if (!layer.style.cluster) return;

    const clusterScale = parseFloat(layer.style.cluster.clusterScale);

    // Spread cluster style into feature.style.
    feature.style = {
      ...feature.style,
      ...layer.style.cluster,
    };

    // Cluster icons will NOT scale different to single locations if the clusterScale is not set in the cluster style.
    if (clusterScale) {
      // The clusterScale will be added to the icon scale.
      feature.style.clusterScale = layer.style.cluster.logScale
        ? // A natural log will be applied to the cluster scaling.
          (Math.log(clusterScale) / Math.log(layer.max_size)) *
          Math.log(feature.properties.size || feature.properties.count)
        : // A fraction of the icon clusterScale will be added to the items scale for all but the biggest cluster location.
          1 +
          (clusterScale / layer.max_size) *
            (feature.properties.size || feature.properties.count);
    }

    // Setting a zoomInScale will INCREASE the scale of icons on higher zoom levels.
    if (layer.style.cluster.zoomInScale) {
      feature.style.zoomInScale =
        layer.style.cluster.zoomInScale * layer.mapview.Map.getView().getZoom();
    }

    // Setting a zoomOutScale will DECREASE the scale of icons on higher zoom levels.
    if (layer.style.cluster.zoomOutScale) {
      feature.style.zoomOutScale =
        layer.style.cluster.zoomOutScale /
        layer.mapview.Map.getView().getZoom();
    }
  }

  /**
  @function highlightStyle
  @description
  Applies highlight style to the feature based on layer configuration.

  @param {Object} feature The feature object.
  */
  function highlightStyle(feature) {
    // Layer must have a highlight style.
    if (!layer.style.highlight) return;

    // Layer must have a highlighted feature stored as layer.highlighted.
    if (!layer.highlight) return;

    // The layer.highlight must be a match for the feature ID.
    if (layer.highlight !== (feature.get('id') || feature.getId())) return;

    feature.style = {
      ...feature.style,
      ...layer.style.highlight,
    };
  }

  /**
  @function labelStyle
  @description
  Applies label style to the feature based on layer configuration.

  @param {Object} feature The feature object.
  */
  function labelStyle(feature) {
    // A feature requires properties to create a label.
    if (!feature.properties) return;

    // Only styled features can be labelled.
    if (!feature.style) return;

    // The label must be displayed.
    if (!layer.style.label?.display) {
      delete feature.style.label;
      return;
    }

    feature.style.label = structuredClone(layer.style.label);

    // Assign count value as text if label.count is truthy.
    feature.style.label.text =
      (feature.style.label.count &&
        feature.properties.count > 1 &&
        feature.properties.count) ||
      undefined;

    feature.style.label.text ??=
      feature.properties[feature.style.label.field] || feature.properties.label;

    // Delete style.label if minZoom exceeds current zoom.
    feature.style.label?.minZoom > layer.mapview.Map.getView().getZoom() &&
      delete feature.style.label;

    // Delere style.label if current zoom exceeds maxZoom.
    feature.style.label?.maxZoom < layer.mapview.Map.getView().getZoom() &&
      delete feature.style.label;
  }

  /**
  @function selectedStyle
  @description
  Applies selected style to the feature based on layer configuration.

  @param {Object} feature The feature object.
  */
  function selectedStyle(feature) {
    // Return before lookup in mapview.locations object.
    if (layer.style.selected === undefined) return;

    // Check whether the feature referenced in mapview.locations
    if (layer.mapview?.locations[`${layer.key}!${feature.properties.id}`]) {
      feature.style = layer.style.selected;
    }
  }
}