ui_elements_drawing.mjs

/**
### /ui/elements/drawing

Exports function which create various drawing elements

Dictionary entries:
- draw_dialog_title
- draw_dialog_begin_drawing
- draw_dialog_cancel_drawing
- draw_dialog_remove_vertex
- draw_dialog_save
- draw_dialog_save_single
- draw_point
- draw_position
- draw_polygon
- draw_rectangle
- circle_config
- draw_circle
- draw_circle_2pt
- radius
- units
- draw_line
- create

@requires /dictionary

@module /ui/elements/drawing
*/

export default {
  circle,
  circle_2pt,
  drawOnclick,
  line,
  locator,
  point,
  polygon,
  rectangle,
};

/**
@function drawOnclick

@description
The drawOnClick method is triggered by clicking on a drawing element button.

The 'active' class is toggled on the button element. The drawing interaction is finished if the 'active' class is toggled off.

A callback method is assigned to the interaction before the interaction object is passed as argument to call the mapview's draw interaction.

@param {Object} e The click event. 
@param {layer} layer Decorated Mapp Layer.
@param {Object} interaction Mapview drawing interaction.

@property {Object} e.target The click event target [button].
*/

function drawOnclick(e, layer, interaction) {
  const btn = e.target;

  if (!btn.classList.toggle('active')) {
    layer.mapview.interaction.finish();
    return;
  }

  !layer.display && layer.show();

  interaction.callback ??= (feature) => {
    mapp.location.create(feature, interaction, layer);

    btn.classList.remove('active');

    delete layer.mapview.interaction;

    mapp.ui.elements.helpDialog();

    // Set highlight interaction if no other interaction is current after 400ms.
    setTimeout(() => {
      !layer.mapview.interaction && layer.mapview.interactions.highlight();
    }, 400);
  };

  layer.mapview.interactions.draw(interaction);

  interaction.helpDialog.header = mapp.utils
    .html`<h3 style="line-height: 2em; margin-right: 1em">${mapp.dictionary.draw_dialog_title}</h3>`;

  interaction.helpDialog.data_id = 'dialog_drawing';

  mapp.ui.elements.helpDialog(interaction.helpDialog);

  btn.classList.add('active');
}

/**
@function point
@description Creates a button for drawing a point on the map.
@param {layer} layer Decorated MAPP Layer.
@return {HTMLElement} The button element.
*/
function point(layer) {
  const helpDialog = {
    content: mapp.utils.html.node`<li>
    <ul>${mapp.dictionary.draw_dialog_begin_drawing}</ul>
    <ul>${mapp.dictionary.draw_dialog_save_single}</ul>`,
  };

  // Set the default values
  layer.draw.point = {
    helpDialog,
    label: mapp.dictionary.draw_point,
    layer,
    type: 'Point',
    ...layer.draw.point,
  };

  // Create the button
  layer.draw.point.btn = mapp.utils.html.node`
    <button
      class="action wide"
      onclick=${(e) => drawOnclick(e, layer, layer.draw.point)}>
      <span class="material-symbols-outlined">add_location_alt</span>
      ${layer.draw.point.label}`;

  return layer.draw.point.btn;
}

/**
@function line
@description Creates a button for drawing a line on the map.
@param {layer} layer Decorated MAPP Layer.
@return {HTMLElement} The button element.
*/
function line(layer) {
  const helpDialog = {
    content: mapp.utils.html.node`<li>
    <ul>${mapp.dictionary.draw_dialog_begin_drawing}</ul>
    <ul>${mapp.dictionary.draw_dialog_remove_vertex}</ul>
    <ul>${mapp.dictionary.draw_dialog_save_single}</ul>`,
  };

  // Set the default values
  layer.draw.line = {
    helpDialog,
    label: mapp.dictionary.draw_line,
    layer,
    type: 'LineString',
    ...layer.draw.line,
  };

  // Create the button
  layer.draw.line.btn = mapp.utils.html.node`
    <button
      class="action wide"
      onclick=${(e) => drawOnclick(e, layer, layer.draw.line)}>
      <span class="material-symbols-outlined">polyline</span>
      ${layer.draw.line.label}`;

  return layer.draw.line.btn;
}

/**
@function polygon
@description Creates a button for drawing a polygon on the map.
@param {layer} layer Decorated MAPP Layer.
@return {HTMLElement} The button element.
*/
function polygon(layer) {
  const helpDialog = {
    content: mapp.utils.html.node`<li>
    <ul>${mapp.dictionary.draw_dialog_begin_drawing}</ul>
    <ul>${mapp.dictionary.draw_dialog_cancel_drawing}
    <ul>${mapp.dictionary.draw_dialog_remove_vertex}</ul>
    <ul>${mapp.dictionary.draw_dialog_save}</ul>`,
  };

  // Set the default values
  layer.draw.polygon = {
    helpDialog,
    label: mapp.dictionary.draw_polygon,
    layer,
    type: 'Polygon',
    ...layer.draw.polygon,
  };

  // Create the button
  layer.draw.polygon.btn = mapp.utils.html.node`
    <button
      class="action wide"
      onclick=${(e) => drawOnclick(e, layer, layer.draw.polygon)}>
      <span class="material-symbols-outlined">activity_zone</span>
      ${layer.draw.polygon.label}`;

  return layer.draw.polygon.btn;
}

/**
@function rectangle
@description Creates a button for rectangle a line on the map.
@param {layer} layer Decorated MAPP Layer.
@return {HTMLElement} The button element.
*/
function rectangle(layer) {
  const helpDialog = {
    content: mapp.utils.html.node`<li>
    <ul>${mapp.dictionary.draw_dialog_begin_drawing}</ul>
    <ul>${mapp.dictionary.draw_dialog_save_single}</ul>`,
  };

  // Set the default values
  layer.draw.rectangle = {
    geometryFunction: ol.interaction.Draw.createBox(),
    helpDialog,
    label: mapp.dictionary.draw_rectangle,
    layer,
    type: 'Circle',
    ...layer.draw.rectangle,
  };

  // Create the button
  layer.draw.rectangle.btn = mapp.utils.html.node`
  <button
    class="action wide"
    onclick=${(e) => drawOnclick(e, layer, layer.draw.rectangle)}>
    <span class="material-symbols-outlined">rectangle</span>
    ${layer.draw.rectangle.label}`;

  return layer.draw.rectangle.btn;
}

/**
@function circle_2pt
@description Creates a button for circle_2pt a line on the map.
@param {layer} layer Decorated MAPP Layer.
@return {HTMLElement} The button element.
*/
function circle_2pt(layer) {
  const helpDialog = {
    content: mapp.utils.html.node`<li>
    <ul>${mapp.dictionary.draw_dialog_begin_drawing}</ul>
    <ul>${mapp.dictionary.draw_dialog_save_single}</ul>`,
  };

  // Set the default values
  layer.draw.circle_2pt = {
    geometryFunction: ol.interaction.Draw.createRegularPolygon(33),
    helpDialog,
    label: mapp.dictionary.draw_circle_2pt,
    layer,
    type: 'Circle',
    ...layer.draw.circle_2pt,
  };

  // Create the button
  layer.draw.circle_2pt.btn = mapp.utils.html.node`
  <button
    class="action wide"
    onclick=${(e) => drawOnclick(e, layer, layer.draw.circle_2pt)}>
    <span class="material-symbols-outlined">outbound</span>
    ${layer.draw.circle_2pt.label}`;

  return layer.draw.circle_2pt.btn;
}

/**
@function circle
@description 
Create a drawer with config interface elements for circle construction.
@param {layer} layer Decorated MAPP Layer.
@return {HTMLElement} A drawer element with controls for circle configuration.
*/
function circle(layer) {
  const helpDialog = {
    content: mapp.utils.html.node`<li>
    <ul>${mapp.dictionary.draw_dialog_begin_drawing}</ul>
    <ul>${mapp.dictionary.draw_dialog_save_single}</ul>`,
  };

  // Set the default values
  const circle = {
    helpDialog,
    label: mapp.dictionary.draw_circle,
    layer,
    type: 'Point',
    units: 'meter',
    unitsConfig: {
      meter: {
        max: 1000,
        min: 1,
        title: 'Meter',
      },
      km: {
        max: 10,
        min: 1,
        title: 'KM',
      },
      miles: {
        max: 10,
        min: 1,
        title: 'Miles',
      },
      meter2: {
        max: 1000,
        min: 1,
        title: 'Meter²',
      },
      km2: {
        max: 10,
        min: 1,
        title: 'KM²',
      },
    },
    unitConversion: {
      km: (v) => v * 1000,
      km2: (v) => Math.sqrt((v * 1000000) / Math.PI),
      meter: (v) => v,
      meter2: (v) => Math.sqrt(v / Math.PI),
      miles: (v) => v * 1609.34,
    },
    ...layer.draw.circle,
  };

  layer.draw.circle = circle;

  circle.unitsOptions ??= Object.keys(circle.unitsConfig);

  circle.unitsOptions = circle.unitsOptions.filter((option) => {
    if (Object.hasOwn(circle.unitsConfig, option)) {
      circle.unitsConfig[option].option = option;
      circle.unitsConfig[option].title ??= option;

      return true;
    } else {
      console.warn(`Unknown circle drawing unit ${option}`);
      return false;
    }
  });

  //Check for whether any valid config options were supplied
  //If not fallback to the full list
  circle.unitsOptions.length &&
    Object.keys(circle.unitsConfig).forEach((config) => {
      if (!circle.unitsOptions.includes(config)) {
        delete circle.unitsConfig[config];
      }
    });

  if (!Object.hasOwn(circle.unitsConfig, circle.units)) {
    circle.units = Object.keys(circle.unitsConfig)[0];
  }

  circle.geometryFunction ??= (coordinates) => {
    const polygonCircular = new ol.geom.Polygon.circular(
      ol.proj.toLonLat(coordinates),
      layer.draw.circle.unitConversion[layer.draw.circle.units](
        layer.draw.circle.radius,
      ),
      64,
    );

    return polygonCircular.transform('EPSG:4326', 'EPSG:3857');
  };

  circle.unitsDropDown = mapp.utils.html.node`<div 
    style="display: grid; grid-template-columns: 100px 1fr; align-items: center;">
      <div style="grid-column: 1;">${mapp.dictionary.units}</div>
      <div style="grid-column: 2;">
        ${mapp.ui.elements.dropdown({
          callback: (e, entry) => setUnits(circle, entry.option),
          entries: Object.values(circle.unitsConfig),
          placeholder: circle.unitsConfig[circle.units].title,
        })}`;

  circle.rangeSlider = mapp.utils.html.node`<div>`;

  setUnits(circle, circle.units);

  circle.btn = mapp.utils.html.node`<button
    class="action wide"
    onclick=${(e) => drawOnclick(e, layer, circle)}>
      <span class="material-symbols-outlined">add_circle</span>
      ${circle.label}`;

  const content = mapp.utils.html.node`
    <div class="panel flex-col">
      ${circle.unitsDropDown}
      ${circle.rangeSlider}
      ${circle.btn}`;

  // The config elements are not shown.
  if (circle.hidePanel) return circle.btn;

  if (circle.drawer === false) {
    return mapp.utils.html`<h3>${mapp.dictionary.circle_config}</h3>${content}`;
  }

  circle.drawer = mapp.ui.elements.drawer({
    header: mapp.utils.html`
    <h3>${mapp.dictionary.circle_config}</h3>
    <div class="material-symbols-outlined caret"/>`,
    content,
  });

  return circle.drawer;
}

/**
@function setUnits
@description 
The setUnits method updates the circle draw config from an units option.
*/
function setUnits(circle, option) {
  const entry = circle.unitsConfig[option];

  circle.units = entry.option;

  circle.radius ??= entry.min;

  // Update the value of the slider to ensure it is within the new min and max values.
  circle.radius = circle.radius > entry.max ? entry.max : circle.radius;

  // Render the slider after changes
  mapp.utils.render(
    circle.rangeSlider,
    mapp.ui.elements.slider({
      callback: (e) => {
        circle.radius = parseFloat(e);
      },
      max: entry.max,
      min: entry.min,
      val: circle.radius,
    }),
  );
}

/**
@function locator
@description Creates a button for drawing a point at your current location.
@param {layer} layer Decorated MAPP Layer.
@return {HTMLElement} The button element.
*/
function locator(layer) {
  layer.draw.locator = {
    label: mapp.dictionary.draw_position,
    layer,
    type: 'Point',
    ...layer.draw.locator,
  };

  layer.draw.locator.btn = mapp.utils.html.node`
    <button
      class="action wide"  
      onclick=${(e) => {
        mapp.utils.getCurrentPosition(async (pos) => {
          const location = {
            layer: layer,
            new: true,
            table: layer.tableCurrent(),
          };

          const coords = ol.proj.transform(
            [parseFloat(pos.coords.longitude), parseFloat(pos.coords.latitude)],
            'EPSG:4326',
            `EPSG:${layer.srid}`,
          );

          location.id = await mapp.utils.xhr({
            body: JSON.stringify({
              [layer.geom]: {
                coordinates: coords,
                type: 'Point',
              },
            }),
            method: 'POST',
            url:
              `${layer.mapview.host}/api/query?` +
              mapp.utils.paramString({
                layer: layer.key,
                locale: layer.mapview.locale.key,
                table: location.table,
                template: 'location_new',
              }),
          });

          mapp.location.get(location);
        });
      }}>
      <span class="material-symbols-outlined">my_location</span>
      ${layer.draw.locator.label}`;

  return layer.draw.locator.btn;
}