ui_locations_entries_layer.mjs

/**
## ui/locations/entries/layer

The entries layer module exports the default layer method.

@module /ui/locations/entries/layer
*/

/**
@function layer

@description
The layer entry method attempts to lookup a layer from the locale and will spread the infoj-entry object into a structured clone of the locale layer.

The infoj-entry effectively becomes a JSON layer which will be decorated and add the mapview.

The layer.key is a concatenated from the entry.layer [key] and the entry.location.hook.

A layer.view will be created for the decorated layer and appended to the entry.node for the location view.

@param {infoj-entry} entry type:layer entry.
@property {string} [entry.layer] lookup layer key for locale.layers[].

@return {HTMLElement} Node element to hold the layer view drawer.
*/
export default function layer(entry) {

  entry.mapview ??= entry.location.layer.mapview

  //An entry needs to have a featureLookup array as it's used in the show method,
  entry.featureLookup ??= []

  // The layer lookup is optional. An entry maybe defined as a layer.
  if (entry.layer) {

    // The layer.key must be unique.
    entry.key = `${entry.layer}|${entry.location.hook}`

    // The locale layer key.
    entry.Key = entry.layer

    // Find JSON layer in locale
    const layer = entry.mapview.locale.layers
      .find(layer => layer.key === entry.layer)

    if (!layer) {

      console.warn(`Layer [${entry.layer}] not found in mapview.locale`)
      return;
    }

    // Spread locale layer into entry.
    entry = {
      ...structuredClone(layer),
      ...entry
    }

    // The assignment should only happen once.
    delete entry.layer
  }

  entry.zIndex ??= entry.location.layer.zIndex++

  entry.show ?? decorateLayer(entry)

  entry.panel ??= mapp.utils.html.node`<div class="entry-layer">`

  return entry.panel
}

/**
@function decorateLayer
@async

@description
The infoj layer entry provided as argument will be decorated to become a mapp layer.

@param {infoj-entry} entry type:layer entry.
*/
async function decorateLayer(entry) {

  await mapp.layer.decorate(entry)

  entry.show = showLayer;

  entry.hide = hideLayer;

  entry.mapview.layers[entry.key] = entry

  entry.display_toggle = mapp.ui.elements.chkbox({
    data_id: `${entry.key}-display`,
    label: entry.name,
    checked: entry.display,
    onchange: (checked) => {
      checked ? entry.show() : entry.hide()
    }
  })

  // The layer may be zoom level restricted.
  if (entry.tables) {

    if (!entry.tableCurrent()) entry.display_toggle.classList.add('disabled');

    entry.mapview.Map.getTargetElement().addEventListener('changeEnd', () => {

      entry.tableCurrent()
        ? entry.display_toggle.classList.remove('disabled')
        : entry.display_toggle.classList.add('disabled')
    })
  }

  entry.panel.append(entry.display_toggle)

  entry.style.elements ??= [
    'labels',
    'label',
    'hovers',
    'hover',
    'icon_scaling',
    'themes',
    'theme',
  ]

  // Request style.panel element as content for drawer.
  entry.style.panel = mapp.ui.elements.layerStyle.panel(entry)

  entry.style.panel && entry.panel.append(entry.style.panel)

  entry.location.removeCallbacks.push(() => {
    entry.hide()
    delete entry.mapview.layers[entry.key]
  })

  entry.display && entry.show()
}

/**
@function showLayer
@async

@description
A custom show [layer] method bound to the [layer] entry which will not update the mapp.hooks.

The showLayer method assigns the entry value as data property. A query will be executed to populate the data property otherwise. The entry will be disabled if the query does not return a response.

The features may be defined as an featureSet in which case the features whose ID is not in the featureSet will not be styled by the [featureStyle]{@link module:/layer/featureStyle~featureProperties} method.

An array feature object can be assigned to featureLookup property. Feature properties found by their ID in the featureLookup array will be assigned to the feature.properties in the featureStyle method.
*/
async function showLayer() {

  const entry = this

  entry.data = entry.value

  if (!entry.data && entry.query) {

    const paramString = mapp.utils.paramString(mapp.utils.queryParams(entry))

    entry.data = await mapp.utils.xhr(`${entry.mapview.host}/api/query?${paramString}`)

    if (!entry.data) {

      entry.view.classList.add('disabled')
      return;
    }
  }

  if (entry.featureSet && Array.isArray(entry.data)) {

    // layer entry may have featureLookup or featureSet but not both.
    delete entry.featureLookup
    entry.featureSet = new Set(entry.data)

  } else {

    // featureLookup on layer must be arrays
    entry.featureLookup = Array.isArray(entry.data) ? entry.data : [entry.data]
  }

  entry.display = true;

  try {

    // Add layer to map
    entry.mapview.Map.addLayer(entry.L);
  } catch {
    // Will catch assertation error when layer is already added.
  }

  // Reload layer data if available.
  entry.reload instanceof Function && entry.reload();

  // Execute showCallbacks
  entry.showCallbacks?.forEach(fn => fn instanceof Function && fn(entry));
}

/**
@function hideLayer

@description
A custom hide [layer] method bound to the [layer] entry which will not update the mapp.hooks.
*/
function hideLayer() {

  this.display = false;

  // Remove OL layer from mapview.
  this.mapview.Map.removeLayer(this.L);

  // Execute hideCallbacks
  this.hideCallbacks?.forEach(fn => fn instanceof Function && fn(this));
}