ui_elements_legendIcon.mjs

/**
### /ui/elements/legendIcon

The legendIcon module exports the default legendIcon(style) method.

@module /ui/elements/legendIcon
*/

const xmlSerializer = new XMLSerializer();

/**
@function legendIcon

@description
The legendIcon method returns an icon for displaying a mapp-style object outside the mapview.Map canvas.

@param {feature-style} style A JSON style object.

@returns {HTMLElement} A HTML element for the style.
*/
export default function legendIcon(style) {

  if (Array.isArray(style.icon)) {
    return createIconFromArray(style);
  }

  if (style.svg || style.type || style.icon) {
    return createIconFromInlineStyle(style);
  }

  if (!style.fillColor) {
    return createLineSymbol(style);
  }

  if (style.fillColor) {
    return createPolygonSymbol(style);
  }
};

/**
@function createIconFromArray

@description
The createIconFromArray method iterates through an `style.icon[]` array to create a layered and scaled icon element for displaying an icon style.

@param {feature-style} style A JSON style object.

@returns {HTMLElement} A HTML element for the style.
*/

function createIconFromArray(style) {

  const canvas = document.createElement('canvas');
  canvas.width = style.width;
  canvas.height = style.height;

  let toLoad = style.icon.length;

  // Images must be loaded in imageStyle image object before they can be applied to canvas.
  function onImgLoad() {
    if (--toLoad) return;

    const vectorContext = ol.render.toContext(canvas.getContext('2d'), {
      size: [style.width, style.height],
      pixelRatio: 1,
    });

    // Styles can not be assigned as array to convas context.
    style.icon.forEach((icon) => {
      vectorContext.setStyle(icon.legendStyle);
      vectorContext.drawGeometry(new ol.geom.Point([canvas.width * 0.5, canvas.height * 0.5]));
    });
  };

  const legendScale = style.icon[0].legendScale || 1;

  style.icon.forEach((icon) => {

    if (icon.type && Object.hasOwn(mapp.utils.svgSymbols, icon.type)) {
      icon.url = mapp.utils.svgSymbols[icon.type](icon);
    }

    const imageStyle = new ol.style.Icon({
      src: icon.svg || icon.url,
      crossOrigin: 'anonymous',
      scale: legendScale * (icon.scale || 1),
      anchor: icon.legendAnchor || [0.5, 0.5]
    })
   
    icon.legendStyle = new ol.style.Style({
      image: imageStyle
    });

    const img = imageStyle.getImage()

    // Check whether the image is loaded in style.
    if (imageStyle.getImageState() === 2) {

      onImgLoad()

    } else {

      img.addEventListener('load', onImgLoad);
      imageStyle.load()
    }
  });

  return canvas;
}

/**
@function createIconFromInlineStyle

@description
The createIconFromInlineStyle creates an icon from an inline style object.

@param {feature-style} style A JSON style object.

@returns {HTMLElement} A HTML element for the style.
*/

function createIconFromInlineStyle(style) {

  const iconUrl = style.icon?.svg
    || style.svg
    || style.icon?.url
    || style.url
    || mapp.utils.svgSymbols[style.icon?.type
    || style.type](style.icon || style)

  const inlineStyle = `
    background-position: center;
    background-repeat: no-repeat;
    background-size: contain;
    width: ${style.width + 'px' || '100%'};
    height: ${style.height + 'px' || '100%'};
    background-image: url(${iconUrl})`;

  return mapp.utils.html.node`<div style=${inlineStyle}>`;
}

/**
@function createLineSymbol

@description
The createLineSymbol creates an icon for a stroke [line] style object.

@param {feature-style} style A JSON style object.

@returns {HTMLElement} A HTML element for the style.
*/

function createLineSymbol(style) {

  const path = `M 0,${style.height / 2} L ${style.width / 2},${style.height / 2} ${style.width / 2},${style.height / 2} ${style.width},${style.height / 2}`;

  const icon = mapp.utils.svg.node`
  <svg 
    height=${style.height} 
    width=${style.width}>
    <path
      d=${path}
      fill="none"
      stroke=${style.strokeColor}
      stroke-width=${style.strokeWidth || 1}/>`;

  const backgroundImage = `data:image/svg+xml,${encodeURIComponent(xmlSerializer.serializeToString(icon))}`;

  const inlineStyle = `
    background-position: center; 
    background-repeat: no-repeat; 
    background-size: contain; 
    width: ${style.width}px; 
    height: ${style.height}px; 
    background-image: url(${backgroundImage});`;

  return mapp.utils.html`<div style=${inlineStyle}>`;
}

/**
@function createPolygonSymbol

@description
The createPolygonSymbol creates an icon for a fill [polygon] style object.

@param {feature-style} style A JSON style object.

@returns {HTMLElement} A HTML element for the style.
*/

function createPolygonSymbol(style) {

  const icon = mapp.utils.svg.node`
  <svg 
    height=${style.height}
    width=${style.width}>
    <rect
      x=${style.strokeWidth || 1}
      y=${style.strokeWidth || 1}
      rx="4px"
      ry="4px"
      stroke-linejoin="round"
      width=${style.width - 2 * (style.strokeWidth || 1)}
      height=${style.height - 2 * (style.strokeWidth || 1)}
      fill=${style.fillColor}
      fill-opacity=${style.fillOpacity || 1}
      stroke=${style.strokeColor}
      stroke-width=${style.strokeWidth || 1}>`;

  const backgroundImage = `data:image/svg+xml,${encodeURIComponent(xmlSerializer.serializeToString(icon))}`;

  const inlineStyle = `
    background-position: center;
    background-repeat: no-repeat;
    background-size: contain;
    width: ${style.width}px;
    height: ${style.height}px;
    background-image: url(${backgroundImage});`;

  return mapp.utils.html`<div style=${inlineStyle}>`;
}