ui_layers_view.mjs

/**
### /ui/layers/view

The module exports a method to create layer.view elements.

@module /ui/layers/view
*/

mapp.utils.merge(mapp.dictionaries, {
  en: {
    layer_zoom_to_extent: 'Zoom to filtered layer extent',
    layer_visibility: 'Toggle visibility',
    zoom_to: 'Zoom to',
  },
  de: {
    layer_zoom_to_extent: 'Zoom zum Ausmaß des gefilterten Datensatzes',
    layer_visibility: 'Umschalten der Ansicht',
    zoom_to: 'Heranzoomen',
  },
  zh: {
    layer_zoom_to_extent: '缩放至筛选图层的相应范围',
    layer_visibility: '可见性切换',
    zoom_to: '缩放至',
  },
  zh_tw: {
    layer_zoom_to_extent: '縮放至篩選圖層的相應範圍',
    layer_visibility: '可見性切換',
    zoom_to: '縮放至',
  },
  pl: {
    layer_zoom_to_extent: 'Przybliż do odfiltrowanej wastwy',
    layer_visibility: 'Zmodyfikuj widzialność',
    zoom_to: 'Przybliż do',
  },
  fr: {
    layer_zoom_to_extent: 'Zoom sur la couche filtrée',
    layer_visibility: 'Modifier la visibilité',
    zoom_to: 'Zoom sur',
  },
  ja: {
    layer_zoom_to_extent: 'フィルターされたレイヤー範囲をズームに',
    layer_visibility: '表示切替',
    zoom_to: 'ズームへ',
  },
  esp: {
    layer_zoom_to_extent: 'Zoom a capa filtrada',
    layer_visibility: 'Alternar visibilidad',
    zoom_to: 'Acercar a',
  },
  tr: {
    layer_zoom_to_extent: 'Filtrelenmis katman kapsamina yaklas',
    layer_visibility: 'Gorunurlugu degistir',
    zoom_to: 'Yaklas',
  },
  it: {
    layer_zoom_to_extent: 'Zoom sul layer',
    layer_visibility: 'Attiva/Disattiva visibilità',
    zoom_to: 'Zoom a',
  },
  th: {
    layer_zoom_to_extent: 'ซูมไปที่ขอบเขตเลเยอร์ที่กรอง',
    layer_visibility: 'สลับการมองเห็น',
    zoom_to: 'ซูมไปที่',
  },
});

/**
@function layerView

@description
The layerView method will create and assign a layer.view node to the layer object.

@param {layer} layer A decorated mapp layer.
@property {Object} layer.view No layer view will be created with the view property null.
@property {Object} layer.drawer The layer view will not be in a drawer element with the drawer property null.

@returns {layer} The layer object is returned after the layer view has been created.
*/
export default function layerView(layer) {
  // Do not create a layer view.
  if (layer.view === null) return layer;

  layer.view = mapp.utils.html.node`<div 
    data-id=${layer.key}
    class="layer-view">`;

  viewConfig(layer);

  // Don't create a drawer element layer view.
  if (layer.drawer === null) {
    // Append elements directly to layer.view
    layer.viewConfig.content.forEach((el) => layer.view.append(el));

    return layer;
  }

  layer.zoomToExtentBtn =
    layer.viewConfig.zoomToExtentBtn &&
    mapp.utils.html`<button
      data-id=zoomToExtent
      title=${mapp.dictionary.layer_zoom_to_extent}
      class="mask-icon fullscreen"
      onclick=${async (e) => {
        // disable button if no locations were found.
        e.target.disabled = !(await layer.zoomToExtent());

        layer.show();
      }}>`;

  // The displayToggle element should be on if the layer is displayed or should be displayed in zoom range.
  const displayToggleClass = `mask-icon toggle ${layer.zoomDisplay || layer.display ? 'on' : ''}`;

  // Create layer.displayToggle button for header.
  layer.displayToggle =
    layer.viewConfig.displayToggle &&
    mapp.utils.html.node`<button
      data-id=display-toggle
      title=${mapp.dictionary.layer_visibility}
      class=${displayToggleClass}
      onclick=${(e) => {
        const toggle = e.target.classList.toggle('on');
        toggle ? layer.show() : layer.hide();
      }}>`;

  // Create a div for the magnifying glass icon
  layer.zoomBtn =
    layer.viewConfig.zoomBtn &&
    mapp.utils.html.node`<button 
      data-id="zoom-to"
      title=${mapp.dictionary.zoom_to}
      class="mask-icon search"
      onclick=${() => zoomToRange(layer)}>`;

  // Add on callback for toggle button.
  layer.showCallbacks.push(() => {
    layer.displayToggle.classList.add('on');
  });

  // Remove on callback for toggle button.
  layer.hideCallbacks.push(() => {
    !layer.zoomDisplay && layer.displayToggle.classList.remove('on');
  });

  const header = mapp.utils.html`
    <h2>${layer.name || layer.key}</h2>
    ${layer.zoomToExtentBtn || ''}
    ${layer.displayToggle || ''}
    ${layer.zoomBtn || ''}
    <div class="mask-icon expander"></div>`;

  layer.drawer = mapp.ui.elements.drawer({
    data_id: layer.key,
    class: layer.viewConfig.classList,
    header,
    content: layer.viewConfig.content,
  });

  // Render layer.drawer into layer.view
  mapp.utils.render(layer.view, layer.drawer);

  // The layer may be zoom level restricted.
  layer.tables &&
    layer.mapview.Map.getTargetElement().addEventListener('changeEnd', () =>
      changeEnd(layer),
    );

  return layer;
}

/**
@function viewConfig

@description
The viewConfig method will create viewConfig object and update the configuration from legacy properties.

The method will iterate through the panelOrder keys to add panel to the content array. 

@param {layer} layer A decorated mapp layer.
@property {Object} [layer.viewConfig] Control options for elements in the layer.view.
@property {Boolean} [viewConfig.displayToggle=true] Controls whether the layer toggle close is displayed.
@property {Boolean} [viewConfig.zoomBtn=true] Controls whether the zoom magnifying glas is displayed.
@property {Boolean} [layer.zoomBtn] Legacy property for viewConfig.zoomBtn.
@property {Array} [layer.tables] Array of data tables for different zoom level.
@property {Boolean} [viewConfig.zoomToExtentBtn=true] Controls whether the zoom to extent button is displayed.
@property {Array} [viewConfig.panelOrder=['draw-drawer', 'dataviews-drawer', 'filter-drawer', 'style-drawer', 'meta']] Controls which panels are added to the view and in which order, will be assigned from layer.panelOrder if not explicit.
@property {Array} [layer.panelOrder] Legacy configuration for viewConfig.panelOrder.
@property {string} [viewConfig.classList] Classlist string to be added to layer-view drawer element classList.
@property {string} [layer.classList] Legacy property for viewConfig.classList.
*/
function viewConfig(layer) {
  layer.viewConfig ??= {
    displayToggle: true,
    zoomBtn: true,
    zoomToExtentBtn: true,
  };

  // Zoom to extent must be enabled in the layer.filter config.
  if (!layer.filter.zoomToExtent) {
    delete layer.viewConfig.zoomToExtentBtn;
  }

  // The zoomBtn should not be created.
  if (layer.zoomBtn === false) {
    delete layer.viewConfig.zoomBtn;
    delete layer.zoomBtn;
  }

  // The zoomBtn is not possible without a tables config.
  if (!layer.tables) {
    delete layer.viewConfig.zoomBtn;
  }

  // Assign viewConfig.panelOrder from legacy JSON layer config.
  layer.viewConfig.panelOrder ??= layer.panelOrder;

  // Delete legacy panelOrder config
  delete layer.panelOrder;

  if (!Array.isArray(layer.viewConfig.panelOrder)) {
    // Assign default panelOrder if not a config array.
    layer.viewConfig.panelOrder = [
      'draw-drawer',
      'dataviews-drawer',
      'filter-drawer',
      'style-drawer',
      'meta',
    ];
  }

  // Create content from layer view panels and plugins
  layer.viewConfig.content = Object.keys(layer)
    .map(
      (key) => mapp.ui.layers.panels[key] && mapp.ui.layers.panels[key](layer),
    )
    .filter((panel) => typeof panel !== 'undefined');

  if (Array.isArray(layer.viewConfig.panelOrder)) {
    // Sort the content array according to the data-id in the panelOrder array.
    layer.viewConfig.content.sort((a, b) => {
      return layer.viewConfig.panelOrder.findIndex(
        (chk) => chk === a.dataset?.id,
      ) < layer.viewConfig.panelOrder.findIndex((chk) => chk === b.dataset?.id)
        ? 1
        : -1;
    });
  }

  layer.viewConfig.classList ??= '';

  // Add default class for layer-view drawer.
  layer.viewConfig.classList += ` layer-view raised `;

  // Add legacy config for layer-view classList.
  layer.viewConfig.classList += layer.classList || '';

  // Add empty class to prevent expanding an empty drawer.
  layer.viewConfig.classList += layer.viewConfig.content.length
    ? ''
    : ' empty ';
}

/**
@function zoomToRange

@description
The zoomToRange method set layer.mapview zoom to be in range of the layer.tables configuration and calls the [layer.show()]{@link module:/layer/decorate~show} method.

@param {layer} layer A decorated mapp layer.
@property {Object} layer.tables The zoom range table configuration.
*/
function zoomToRange(layer) {
  const minZoom = Object.entries(layer.tables).find((entry) => !!entry[1])[0];

  const maxZoom = Object.entries(layer.tables)
    .reverse()
    .find((entry) => !!entry[1])[0];

  const view = layer.mapview.Map.getView();

  view.getZoom() < minZoom ? view.setZoom(minZoom) : view.setZoom(maxZoom);

  layer.show();
}

/**
@function changeEnd

@description
The changeEnd method is assigned to the mapview.Map changeEnd event listener for layer with a tables object.

Layer with a tables object maybe zoom restricted.

The [layer.tableCurrent()]{@link module:/layer/decorate~tableCurrent} method is called to determine whether the layer has a table configured for the current zoom level.

Nested elements will be disabled if a layer can not be displayed.

@param {layer} layer A decorated mapp layer.
@property {HTMLElement} layer.view The layer view element.
@property {HTMLElement} layer.drawer The layer view drawer element.
@property {HTMLElement} layer.displayToggle A button element which toggles the layer.display.
@property {HTMLElement} layer.zoomBtn A button element which sets the mapview into the visible zoom range for the layer.
*/
function changeEnd(layer) {
  // Get array of nested elements excluding the .header element.
  const nestedElements = Array.from(
    layer.drawer.querySelectorAll(':scope > :not(.header)'),
  );

  // Check whether the layer can be displayed at current zoom.
  if (layer.tableCurrent()) {
    // The zoomBtn is hidden if the layer is in range.
    layer.zoomBtn instanceof HTMLElement &&
      layer.zoomBtn.style.setProperty('display', 'none');

    // Enable layer display toggle.
    layer.displayToggle instanceof HTMLElement &&
      layer.displayToggle.classList.remove('disabled');

    // Enable all drawer elements in array.
    nestedElements.forEach((el) => {
      el.disabled = false;
      el.classList.remove('disabled');
    });
  } else {
    // The zoomBtn is displayed outside if the layer is out of range.
    layer.zoomBtn instanceof HTMLElement &&
      layer.zoomBtn.style.setProperty('display', 'block');

    // Collapse drawer and disable layer.view.
    layer.view.querySelector('.layer-view.drawer').classList.remove('expanded');

    // Disable layer display toggle.
    layer.displayToggle instanceof HTMLElement &&
      layer.displayToggle.classList.add('disabled');

    // Disable all drawer elements in array.
    nestedElements.forEach((el) => {
      el.disabled = true;
      el.classList.add('disabled');
    });
  }
}