ui_elements_drawing.mjs

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

@module /ui/elements/drawing
*/

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

mapp.utils.merge(mapp.dictionaries, {
  en: {
    draw_dialog_title: 'Drawing Instructions',
    draw_dialog_begin_drawing: 'Begin Drawing - Click anywhere on the map',
    draw_dialog_cancel_drawing: 'Cancel Drawing - ESC Key',
    draw_dialog_remove_vertex: 'Remove Last Vertex - Right Click',
    draw_dialog_save: 'Save - Double Click',
    draw_dialog_save_single: 'Save - Single Click',
    draw_point: 'Point',
    draw_position: 'Current Position',
    draw_polygon: 'Polygon',
    draw_rectangle: 'Rectangle',
    circle_config: 'Circle configuration',
    draw_circle: 'Circle from Centre',
    draw_circle_2pt: 'Manual Circle',
    radius: 'Radius',
    units: 'Units',
    draw_line: 'Line',
    create: 'Create',
  },
  de: {
    draw_point: 'Punkt',
    draw_position: 'Aktueller Standort',
    draw_polygon: 'Polygon',
    draw_rectangle: 'Rechteck',
    circle_config: 'Zirkel Einstellung',
    draw_circle: 'Zirkel',
    draw_circle_2pt: '2 Punkt Zirkel',
    radius: 'Radius',
    units: 'Masseinheit',
    draw_line: 'Linie',
    create: 'Erstellen',
  },
  zh: {
    draw_point: '点',
    draw_position: '当前位置',
    draw_polygon: '多边形',
    draw_rectangle: '长方形',
    circle_config: '圆形调整',
    draw_circle: '由中心点外沿绘制圆形',
    draw_circle_2pt: '手绘圆形',
    radius: '半径',
    units: '单位',
    draw_line: '线条',
    create: '创建',
  },
  zh_tw: {
    draw_point: '點',
    draw_position: '當前位置',
    draw_polygon: '多邊形',
    draw_rectangle: '長方形',
    circle_config: '圓形調整',
    draw_circle: '由中心點外沿繪製圓形',
    draw_circle_2pt: '手繪圓形',
    radius: '半徑',
    units: '單位',
    draw_line: '線條',
    create: '創建',
  },
  pl: {
    draw_dialog_title: 'Instrukcja obsługi',
    draw_dialog_begin_drawing: 'Rozpoczęcie rysowania - kliknij w dowolne miejsce na mapie',
    draw_dialog_cancel_drawing: 'Usunięcie rysunku - kliknąć klawisz ESC',
    draw_dialog_remove_vertex: 'Usuń ostatni wierzchołek - Kliknij prawym przyciskiem myszy',
    draw_dialog_save: 'Zapisz - podwójne kliknięcie',
    draw_dialog_save_single: 'Zapisz - pojedyncze kliknięcie',
    draw_point: 'Punkt',
    draw_position: 'Aktualna pozycja',
    draw_polygon: 'Poligon',
    draw_rectangle: 'Prostokąt',
    circle_config: 'Konfiguracja okręgu',
    draw_circle: 'Okrąg od centrum',
    draw_circle_2pt: 'Okrąg odręczny',
    radius: 'Promień',
    units: 'Jednostki',
    draw_line: 'Linia',
    create: 'Utwórz',
  },
  fr: {
    draw_point: 'Point',
    draw_position: 'Position Actuelle',
    draw_polygon: 'Polygone',
    draw_rectangle: 'Rectangle',
    circle_config: 'Paramétrage du cercle',
    draw_circle: 'Cercle à partir du centre',
    draw_circle_2pt: 'Cercle Manuel',
    radius: 'Rayon',
    units: 'Unité',
    draw_line: 'Ligne',
    create: 'Créer',
  },
  ja: {
    draw_point: 'ポイント',
    draw_position: '現地',
    draw_polygon: 'ポリゴン',
    draw_rectangle: '長方形',
    circle_config: '丸の構成',
    draw_circle: '真ん中からの丸',
    draw_circle_2pt: 'マニュアルの丸',
    radius: '半径',
    units: '単位',
    draw_line: '線',
    create: '作成',
  },
  esp: {
    draw_point: 'Punto',
    draw_position: 'Posición actual',
    draw_polygon: 'Polígono',
    draw_rectangle: 'Rectángulo',
    circle_config: 'Configuración del círculo',
    draw_circle: 'Círculo desde el centro',
    draw_circle_2pt: 'Círculo manual',
    radius: 'Radio',
    units: 'Unidad',
    draw_line: 'Línea',
    create: 'Crear',
  },
  tr: {
    draw_point: 'Nokta',
    draw_position: 'Mevcut konum',
    draw_polygon: 'Poligon',
    draw_rectangle: 'Dikdortgen',
    circle_config: 'Cember ayarlari',
    draw_circle: 'Merkezden cember',
    draw_circle_2pt: 'Manuel cember',
    radius: 'Yaricap',
    units: 'Birimler',
    draw_line: 'Cizgi',
    create: 'Olustur',
  },
  it: {
    draw_point: 'Punto',
    draw_position: 'Posizione attuale',
    draw_polygon: 'Poligono',
    draw_rectangle: 'Rettangolo',
    circle_config: 'Configurazione cerchio',
    draw_circle: 'Cerchio dal centro',
    draw_circle_2pt: 'Cerchio manuale',
    radius: 'Raggio',
    units: 'Unità',
    draw_line: 'Linea',
    create: 'Creare',
  },
  th: {
    draw_point: 'จุด',
    draw_position: 'ตำแหน่งปัจจุบัน',
    draw_polygon: 'รูปหลายเหลี่ยม',
    draw_rectangle: 'สี่เหลี่ยมผืนผ้า',
    circle_config: 'การกำหนดค่าวงกลม',
    draw_circle: 'วงกลมจากศูนย์กลาง',
    draw_circle_2pt: 'วงกลมแบบแมนนวล',
    radius: 'รัศมี',
    units: 'หน่วย',
    draw_line: 'เส้น',
    create: 'สร้าง',
  }
})

/**
@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>${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 = {
    layer,
    label: mapp.dictionary.draw_point,
    helpDialog,
    type: 'Point',
    ...layer.draw.point
  }

  // Create the button
  layer.draw.point.btn = mapp.utils.html.node`
    <button
      class="flat wide bold primary-colour"
      onclick=${e => drawOnclick(e, layer, layer.draw.point)}>
      ${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 = {
    layer,
    label: mapp.dictionary.draw_line,
    helpDialog,
    type: 'LineString',
    ...layer.draw.line
  }

  // Create the button
  layer.draw.line.btn = mapp.utils.html.node`
    <button
      class="flat wide bold primary-colour"
      onclick=${e => drawOnclick(e, layer, layer.draw.line)}>
      ${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 = {
    layer,
    label: mapp.dictionary.draw_polygon,
    helpDialog,
    type: 'Polygon',
    ...layer.draw.polygon
  }

  // Create the button
  layer.draw.polygon.btn = mapp.utils.html.node`
    <button
      class="flat wide bold primary-colour"
      onclick=${e => drawOnclick(e, layer, layer.draw.polygon)}>
      ${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 = {
    layer,
    label: mapp.dictionary.draw_rectangle,
    helpDialog,
    type: 'Circle',
    geometryFunction: ol.interaction.Draw.createBox(),
    ...layer.draw.rectangle
  }

  // Create the button
  layer.draw.rectangle.btn = mapp.utils.html.node`
  <button
    class="flat wide bold primary-colour"
    onclick=${e => drawOnclick(e, layer, layer.draw.rectangle)}>
    ${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 = {
    layer,
    type: 'Circle',
    helpDialog,
    geometryFunction: ol.interaction.Draw.createRegularPolygon(33),
    label: mapp.dictionary.draw_circle_2pt,
    ...layer.draw.circle_2pt
  }

  // Create the button
  layer.draw.circle_2pt.btn = mapp.utils.html.node`
  <button
    class="flat wide bold primary-colour"
    onclick=${e => drawOnclick(e, layer, layer.draw.circle_2pt)}>
    ${layer.draw.circle_2pt.label}`

  return layer.draw.circle_2pt.btn
}

/**
@function circle
@description Creates a button for circle a line on the map.
@param {layer} layer Decorated MAPP Layer.
@return {HTMLElement} The button element.
*/
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
  layer.draw.circle = {
    layer,
    helpDialog,
    type: 'Point',
    units: 'meter',
    radius: 100,
    radiusMin: 1,
    radiusMax: 1000,

    // Methods to transform input radius.
    unitConversion: {
      meter: v => v,
      km: v => v * 1000,
      miles: v => v * 1609.34,
      meter2: v => Math.sqrt(v / Math.PI),
      km2: v => Math.sqrt(v * 1000000 / Math.PI),
    },
    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')
    },
    label: mapp.dictionary.draw_circle,
    ...layer.draw.circle
  }

  // Build an array for the unit, label, min and max values.
  const units = [
    {
      option: 'meter',
      title: 'Meter',
      min: 1,
      max: 1000
    },
    {
      option: 'km',
      title: 'KM',
      min: 1,
      max: 10
    },
    {
      option: 'miles',
      title: 'Miles',
      min: 1,
      max: 10
    },
    {
      option: 'meter2',
      title: 'Meter²',
      min: 1,
      max: 1000
    },
    {
      option: 'km2',
      title: 'KM²',
      min: 1,
      max: 10
    }
  ];

  const 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({
    placeholder: units.find(entry => entry.option === layer.draw.circle.units).title,
    entries: units,
    callback: (e, entry) => {

      layer.draw.circle.units = entry.option;
      layer.draw.circle.radiusMin = entry.min;
      layer.draw.circle.radiusMax = entry.max;

      // Update the value of the slider to ensure it is within the new min and max values.
      layer.draw.circle.radius = layer.draw.circle.radius > layer.draw.circle.radiusMax ? layer.draw.circle.radiusMax : layer.draw.circle.radius;
    
      // Render the slider after changes
      createSlider();
    }
  })}`

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

  createSlider();

/**
@function createSlider
@description Creates a slider for the circle radius, renders this slider into the rangeSlider div, and assigns a callback function to update the radius value.
*/

  function createSlider() {
    mapp.utils.render(rangeSlider, mapp.ui.elements.slider({
      min: layer.draw.circle.radiusMin,
      max: layer.draw.circle.radiusMax,
      val: layer.draw.circle.radius,
      callback: e => {
        layer.draw.circle.radius = parseFloat(e)
      }
    })
    )
  }

  layer.draw.circle.panel = mapp.utils.html.node`
    <div class="panel flex-col">
      ${unitsDropDown}
      ${rangeSlider}`

  layer.draw.circle.btn = mapp.utils.html.node`
  <button
    class="flat wide bold primary-colour"
    onclick=${e => drawOnclick(e, layer, layer.draw.circle)}>
    ${layer.draw.circle.label}`

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

  // Return the config element in a drawer with the interaction toggle button as sibling.
  return mapp.utils.html.node`<div>
    ${mapp.ui.elements.drawer({
    header: mapp.utils.html`
      <h3>${mapp.dictionary.circle_config}</h3>
      <div class="mask-icon expander"></div>`,
    content: layer.draw.circle.panel
  })}${layer.draw.circle.btn}`
}

/**
@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 = {
    layer,
    label: mapp.dictionary.draw_position,
    type: 'Point',
    ...layer.draw.locator
  }

  layer.draw.locator.btn = mapp.utils.html.node`
    <button
      class="flat wide bold primary-colour"  
      onclick=${e => {

      mapp.utils.getCurrentPosition(async (pos) => {

        const location = {
          layer: layer,
          table: layer.tableCurrent(),
          new: true
        }

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

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

        mapp.location.get(location)
      })

    }}>${layer.draw.locator.label}`

  return layer.draw.locator.btn
}