U.FeatureMixin = {
  staticOptions: { mainColor: 'color' },

  getSyncMetadata: function () {
    return {
      subject: 'feature',
      metadata: {
        id: this.id,
        layerId: this.datalayer?.umap_id || null,
        featureType: this.getClassName(),
      },
    }
  },

  onCommit: function () {
    // When the layer is a remote layer, we don't want to sync the creation of the
    // points via the websocket, as the other peers will get them themselves.
    if (this.datalayer?.isRemoteLayer()) return

    // The "endEdit" event is triggered at the end of an edition,
    // and will trigger the sync.
    // In the case of a deletion (or a change of layer), we don't want this
    // event triggered to cause a sync event, as it would reintroduce
    // deleted features.
    // The `._marked_for_deletion` private property is here to track this status.
    if (this._marked_for_deletion === true) {
      this._marked_for_deletion = false
      return
    }
    this.sync.upsert(this.toGeoJSON())
  },

  getGeometry: function () {
    return this.toGeoJSON().geometry
  },

  initialize: function (map, latlng, options, id) {
    this.map = map
    this.sync = map.sync_engine.proxy(this)
    this._marked_for_deletion = false

    if (typeof options === 'undefined') {
      options = {}
    }
    // DataLayer the marker belongs to
    this.datalayer = options.datalayer || null
    this.properties = { _umap_options: {} }

    if (options.geojson) {
      this.populate(options.geojson)
    }

    if (id) {
      this.id = id
    } else {
      let geojson_id
      if (options.geojson) {
        geojson_id = options.geojson.id
      }

      // Each feature needs an unique identifier
      if (U.Utils.checkId(geojson_id)) {
        this.id = geojson_id
      } else {
        this.id = U.Utils.generateId()
      }
    }
    let isDirty = false
    try {
      Object.defineProperty(this, 'isDirty', {
        get: () => isDirty,
        set: (status) => {
          if (!isDirty && status) {
            this.fire('isdirty')
          }
          isDirty = status
          if (this.datalayer) {
            this.datalayer.isDirty = status
          }
        },
      })
    } catch (e) {
      // Certainly IE8, which has a limited version of defineProperty
    }
    this.preInit()
    this.addInteractions()
    this.parentClass.prototype.initialize.call(this, latlng, options)
  },

  preInit: () => {},

  isReadOnly: function () {
    return this.datalayer?.isDataReadOnly()
  },

  getSlug: function () {
    return this.properties[this.map.getOption('slugKey') || 'name'] || ''
  },

  getPermalink: function () {
    const slug = this.getSlug()
    if (slug)
      return `${U.Utils.getBaseUrl()}?${U.Utils.buildQueryString({ feature: slug })}${
        window.location.hash
      }`
  },

  view: function (e) {
    const outlink = this.getOption('outlink')
    const target = this.getOption('outlinkTarget')
    if (outlink) {
      switch (target) {
        case 'self':
          window.location = outlink
          break
        case 'parent':
          window.top.location = outlink
          break
        default:
          window.open(this.properties._umap_options.outlink)
      }
      return
    }
    // TODO deal with an event instead?
    if (this.map.slideshow) {
      this.map.slideshow.current = this
    }
    this.map.currentFeature = this
    this.attachPopup()
    this.openPopup(e?.latlng || this.getCenter())
  },

  render: function (fields) {
    const impactData = fields.some((field) => {
      return field.startsWith('properties.')
    })
    if (impactData) {
      if (this.map.currentFeature === this) {
        this.view()
      }
    }
    this._redraw()
  },

  openPopup: function () {
    this.parentClass.prototype.openPopup.apply(this, arguments)
  },

  edit: function (e) {
    if (!this.map.editEnabled || this.isReadOnly()) return
    const container = L.DomUtil.create('div', 'umap-feature-container')
    L.DomUtil.createTitle(
      container,
      L._('Feature properties'),
      `icon-${this.getClassName()}`
    )

    let builder = new U.FormBuilder(
      this,
      [['datalayer', { handler: 'DataLayerSwitcher' }]],
      {
        callback: function () {
          this.edit(e)
        }, // removeLayer step will close the edit panel, let's reopen it
      }
    )
    container.appendChild(builder.build())

    const properties = []
    let property
    for (let i = 0; i < this.datalayer._propertiesIndex.length; i++) {
      property = this.datalayer._propertiesIndex[i]
      if (L.Util.indexOf(['name', 'description'], property) !== -1) {
        continue
      }
      properties.push([`properties.${property}`, { label: property }])
    }
    // We always want name and description for now (properties management to come)
    properties.unshift('properties.description')
    properties.unshift('properties.name')
    builder = new U.FormBuilder(this, properties, {
      id: 'umap-feature-properties',
    })
    container.appendChild(builder.build())
    this.appendEditFieldsets(container)
    const advancedActions = L.DomUtil.createFieldset(container, L._('Advanced actions'))
    this.getAdvancedEditActions(advancedActions)
    const onLoad = this.map.editPanel.open({ content: container })
    onLoad.then(() => {
      builder.helpers['properties.name'].input.focus()
    })
    this.map.editedFeature = this
    if (!this.isOnScreen()) this.zoomTo(e)
  },

  getAdvancedEditActions: function (container) {
    L.DomUtil.createButton('button umap-delete', container, L._('Delete'), (e) => {
      this.confirmDelete().then(() => this.map.editPanel.close())
    })
  },

  appendEditFieldsets: function (container) {
    const optionsFields = this.getShapeOptions()
    let builder = new U.FormBuilder(this, optionsFields, {
      id: 'umap-feature-shape-properties',
    })
    const shapeProperties = L.DomUtil.createFieldset(container, L._('Shape properties'))
    shapeProperties.appendChild(builder.build())

    const advancedOptions = this.getAdvancedOptions()
    builder = new U.FormBuilder(this, advancedOptions, {
      id: 'umap-feature-advanced-properties',
    })
    const advancedProperties = L.DomUtil.createFieldset(
      container,
      L._('Advanced properties')
    )
    advancedProperties.appendChild(builder.build())

    const interactionOptions = this.getInteractionOptions()
    builder = new U.FormBuilder(this, interactionOptions)
    const popupFieldset = L.DomUtil.createFieldset(
      container,
      L._('Interaction options')
    )
    popupFieldset.appendChild(builder.build())
  },

  getInteractionOptions: () => [
    'properties._umap_options.popupShape',
    'properties._umap_options.popupTemplate',
    'properties._umap_options.showLabel',
    'properties._umap_options.labelDirection',
    'properties._umap_options.labelInteractive',
    'properties._umap_options.outlink',
    'properties._umap_options.outlinkTarget',
  ],

  endEdit: () => {},

  getDisplayName: function (fallback) {
    if (fallback === undefined) fallback = this.datalayer.options.name
    const key = this.getOption('labelKey') || 'name'
    // Variables mode.
    if (U.Utils.hasVar(key))
      return U.Utils.greedyTemplate(key, this.extendedProperties())
    // Simple mode.
    return this.properties[key] || this.properties.title || fallback
  },

  hasPopupFooter: function () {
    if (L.Browser.ielt9) return false
    if (this.datalayer.isRemoteLayer() && this.datalayer.options.remoteData.dynamic)
      return false
    return this.map.getOption('displayPopupFooter')
  },

  getPopupClass: function () {
    const old = this.getOption('popupTemplate') // Retrocompat.
    return U.Popup[this.getOption('popupShape') || old] || U.Popup
  },

  attachPopup: function () {
    const Class = this.getPopupClass()
    this.bindPopup(new Class(this))
  },

  confirmDelete: async function () {
    const confirmed = await this.map.dialog.confirm(
      L._('Are you sure you want to delete the feature?')
    )
    if (confirmed) {
      this.del()
      return true
    }
    return false
  },

  del: function (sync) {
    this.isDirty = true
    this.map.closePopup()
    if (this.datalayer) {
      this.datalayer.removeLayer(this, sync)
    }
  },

  connectToDataLayer: function (datalayer) {
    this.datalayer = datalayer
    this.options.renderer = this.datalayer.renderer
  },

  disconnectFromDataLayer: function (datalayer) {
    if (this.datalayer === datalayer) {
      this.datalayer = null
    }
  },

  cleanProperty: ([key, value]) => {
    // dot in key will break the dot based property access
    // while editing the feature
    key = key.replace('.', '_')
    return [key, value]
  },

  populate: function (feature) {
    this.properties = Object.fromEntries(
      Object.entries(feature.properties || {}).map(this.cleanProperty)
    )
    this.properties._umap_options = L.extend(
      {},
      this.properties._storage_options,
      this.properties._umap_options
    )
    // Retrocompat
    if (this.properties._umap_options.clickable === false) {
      this.properties._umap_options.interactive = false
      delete this.properties._umap_options.clickable
    }
  },

  changeDataLayer: function (datalayer) {
    if (this.datalayer) {
      this.datalayer.isDirty = true
      this.datalayer.removeLayer(this)
    }

    datalayer.addLayer(this)
    this.sync.upsert(this.toGeoJSON())
    datalayer.isDirty = true
    this._redraw()
  },

  getOption: function (option, fallback) {
    let value = fallback
    if (typeof this.staticOptions[option] !== 'undefined') {
      value = this.staticOptions[option]
    } else if (U.Utils.usableOption(this.properties._umap_options, option)) {
      value = this.properties._umap_options[option]
    } else if (this.datalayer) {
      value = this.datalayer.getOption(option, this)
    } else {
      value = this.map.getOption(option)
    }
    return value
  },

  getDynamicOption: function (option, fallback) {
    let value = this.getOption(option, fallback)
    // There is a variable inside.
    if (U.Utils.hasVar(value)) {
      value = U.Utils.greedyTemplate(value, this.properties, true)
      if (U.Utils.hasVar(value)) value = this.map.getDefaultOption(option)
    }
    return value
  },

  zoomTo: function ({ easing, latlng, callback } = {}) {
    if (easing === undefined) easing = this.map.getOption('easing')
    if (callback) this.map.once('moveend', callback.call(this))
    if (easing) {
      this.map.flyTo(this.getCenter(), this.getBestZoom())
    } else {
      latlng = latlng || this.getCenter()
      this.map.setView(latlng, this.getBestZoom() || this.map.getZoom())
    }
  },

  getBestZoom: function () {
    return this.getOption('zoomTo')
  },

  getNext: function () {
    return this.datalayer.getNextFeature(this)
  },

  getPrevious: function () {
    return this.datalayer.getPreviousFeature(this)
  },

  cloneProperties: function () {
    const properties = L.extend({}, this.properties)
    properties._umap_options = L.extend({}, properties._umap_options)
    if (Object.keys && Object.keys(properties._umap_options).length === 0) {
      delete properties._umap_options // It can make a difference on big data sets
    }
    return properties
  },

  deleteProperty: function (property) {
    delete this.properties[property]
    this.makeDirty()
  },

  renameProperty: function (from, to) {
    this.properties[to] = this.properties[from]
    this.deleteProperty(from)
  },

  toGeoJSON: function () {
    const geojson = this.parentClass.prototype.toGeoJSON.call(this)
    geojson.properties = this.cloneProperties()
    geojson.id = this.id
    delete geojson.properties._storage_options
    return geojson
  },

  addInteractions: function () {
    this.on('contextmenu editable:vertex:contextmenu', this._showContextMenu, this)
    this.on('click', this._onClick)
  },

  _onClick: function (e) {
    if (this.map.measureTools?.enabled()) return
    this._popupHandlersAdded = true // Prevent leaflet from managing event
    if (!this.map.editEnabled) {
      this.view(e)
    } else if (!this.isReadOnly()) {
      if (e.originalEvent.shiftKey) {
        if (e.originalEvent.ctrlKey || e.originalEvent.metaKey) {
          this.datalayer.edit(e)
        } else {
          if (this._toggleEditing) this._toggleEditing(e)
          else this.edit(e)
        }
      } else {
        new L.Toolbar.Popup(e.latlng, {
          className: 'leaflet-inplace-toolbar',
          anchor: this.getPopupToolbarAnchor(),
          actions: this.getInplaceToolbarActions(e),
        }).addTo(this.map, this, e.latlng)
      }
    }
    L.DomEvent.stop(e)
  },

  getPopupToolbarAnchor: () => [0, 0],

  getInplaceToolbarActions: (e) => [U.ToggleEditAction, U.DeleteFeatureAction],

  _showContextMenu: function (e) {
    L.DomEvent.stop(e)
    const pt = this.map.mouseEventToContainerPoint(e.originalEvent)
    e.relatedTarget = this
    this.map.contextmenu.showAt(pt, e)
  },

  makeDirty: function () {
    this.isDirty = true
  },

  getMap: function () {
    return this.map
  },

  getContextMenuItems: function (e) {
    const permalink = this.getPermalink()
    let items = []
    if (permalink)
      items.push({
        text: L._('Permalink'),
        callback: () => {
          window.open(permalink)
        },
      })
    if (this.map.editEnabled && !this.isReadOnly()) {
      items = items.concat(this.getContextMenuEditItems(e))
    }
    return items
  },

  getContextMenuEditItems: function () {
    let items = ['-']
    if (this.map.editedFeature !== this) {
      items.push({
        text: `${L._('Edit this feature')} (⇧+Click)`,
        callback: this.edit,
        context: this,
        iconCls: 'umap-edit',
      })
    }
    items = items.concat(
      {
        text: this.map.help.displayLabel('EDIT_FEATURE_LAYER'),
        callback: this.datalayer.edit,
        context: this.datalayer,
        iconCls: 'umap-edit',
      },
      {
        text: L._('Delete this feature'),
        callback: this.confirmDelete,
        context: this,
        iconCls: 'umap-delete',
      },
      {
        text: L._('Clone this feature'),
        callback: this.clone,
        context: this,
      }
    )
    return items
  },

  onRemove: function (map) {
    this.parentClass.prototype.onRemove.call(this, map)
    if (this.map.editedFeature === this) {
      this._marked_for_deletion = true
      this.endEdit()
      this.map.editPanel.close()
    }
  },

  resetTooltip: function () {
    if (!this.hasGeom()) return
    const displayName = this.getDisplayName(null)
    let showLabel = this.getOption('showLabel')
    const oldLabelHover = this.getOption('labelHover')

    const options = {
      direction: this.getOption('labelDirection'),
      interactive: this.getOption('labelInteractive'),
    }

    if (oldLabelHover && showLabel) showLabel = null // Retrocompat.
    options.permanent = showLabel === true
    this.unbindTooltip()
    if ((showLabel === true || showLabel === null) && displayName)
      this.bindTooltip(U.Utils.escapeHTML(displayName), options)
  },

  isFiltered: function () {
    const filterKeys = this.datalayer.getFilterKeys()
    const filter = this.map.browser.options.filter
    if (filter && !this.matchFilter(filter, filterKeys)) return true
    if (!this.matchFacets()) return true
    return false
  },

  matchFilter: function (filter, keys) {
    filter = filter.toLowerCase()
    if (U.Utils.hasVar(keys)) {
      return this.getDisplayName().toLowerCase().indexOf(filter) !== -1
    }
    keys = keys.split(',')
    for (let i = 0, value; i < keys.length; i++) {
      value = `${this.properties[keys[i]] || ''}`
      if (value.toLowerCase().indexOf(filter) !== -1) return true
    }
    return false
  },

  matchFacets: function () {
    const selected = this.map.facets.selected
    for (const [name, { type, min, max, choices }] of Object.entries(selected)) {
      let value = this.properties[name]
      const parser = this.map.facets.getParser(type)
      value = parser(value)
      switch (type) {
        case 'date':
        case 'datetime':
        case 'number':
          if (!Number.isNaN(min) && !Number.isNaN(value) && min > value) return false
          if (!Number.isNaN(max) && !Number.isNaN(value) && max < value) return false
          break
        default:
          value = value || L._('<empty value>')
          if (choices?.length && !choices.includes(value)) return false
          break
      }
    }
    return true
  },

  onVertexRawClick: function (e) {
    new L.Toolbar.Popup(e.latlng, {
      className: 'leaflet-inplace-toolbar',
      actions: this.getVertexActions(e),
    }).addTo(this.map, this, e.latlng, e.vertex)
  },

  getVertexActions: () => [U.DeleteVertexAction],

  isMulti: () => false,

  clone: function () {
    const geoJSON = this.toGeoJSON()
    delete geoJSON.id
    delete geoJSON.properties.id
    const layer = this.datalayer.geojsonToFeatures(geoJSON)
    layer.isDirty = true
    layer.edit()
    return layer
  },

  extendedProperties: function () {
    // Include context properties
    properties = this.map.getGeoContext()
    const locale = L.getLocale()
    if (locale) properties.locale = locale
    if (L.lang) properties.lang = L.lang
    properties.rank = this.getRank() + 1
    if (this._map && this.hasGeom()) {
      center = this.getCenter()
      properties.lat = center.lat
      properties.lon = center.lng
      properties.lng = center.lng
      if (typeof this.getMeasure !== 'undefined') {
        properties.measure = this.getMeasure()
      }
    }
    return L.extend(properties, this.properties)
  },

  getRank: function () {
    return this.datalayer._index.indexOf(L.stamp(this))
  },
}

U.Marker = L.Marker.extend({
  parentClass: L.Marker,
  includes: [U.FeatureMixin],

  preInit: function () {
    this.setIcon(this.getIcon())
  },

  highlight: function () {
    L.DomUtil.addClass(this.options.icon.elements.main, 'umap-icon-active')
  },

  resetHighlight: function () {
    L.DomUtil.removeClass(this.options.icon.elements.main, 'umap-icon-active')
  },

  addInteractions: function () {
    U.FeatureMixin.addInteractions.call(this)
    this.on(
      'dragend',
      function (e) {
        this.isDirty = true
        this.edit(e)
        this.sync.update('geometry', this.getGeometry())
      },
      this
    )
    this.on('editable:drawing:commit', this.onCommit)
    if (!this.isReadOnly()) this.on('mouseover', this._enableDragging)
    this.on('mouseout', this._onMouseOut)
    this._popupHandlersAdded = true // prevent Leaflet from binding event on bindPopup
    this.on('popupopen', this.highlight)
    this.on('popupclose', this.resetHighlight)
  },

  hasGeom: function () {
    return !!this._latlng
  },

  _onMouseOut: function () {
    if (this.dragging?._draggable && !this.dragging._draggable._moving) {
      // Do not disable if the mouse went out while dragging
      this._disableDragging()
    }
  },

  _enableDragging: function () {
    // TODO: start dragging after 1 second on mouse down
    if (this.map.editEnabled) {
      if (!this.editEnabled()) this.enableEdit()
      // Enabling dragging on the marker override the Draggable._OnDown
      // event, which, as it stopPropagation, refrain the call of
      // _onDown with map-pane element, which is responsible to
      // set the _moved to false, and thus to enable the click.
      // We should find a cleaner way to handle this.
      this.map.dragging._draggable._moved = false
    }
  },

  _disableDragging: function () {
    if (this.map.editEnabled) {
      if (this.editor?.drawing) return // when creating a new marker, the mouse can trigger the mouseover/mouseout event
      // do not listen to them
      this.disableEdit()
    }
  },

  _redraw: function () {
    if (this.datalayer?.isVisible()) {
      this._initIcon()
      this.update()
    }
  },

  _initIcon: function () {
    this.options.icon = this.getIcon()
    L.Marker.prototype._initIcon.call(this)
    // Allow to run code when icon is actually part of the DOM
    this.options.icon.onAdd()
    this.resetTooltip()
  },

  _getTooltipAnchor: function () {
    const anchor = this.options.icon.options.tooltipAnchor.clone()
    const direction = this.getOption('labelDirection')
    if (direction === 'left') {
      anchor.x *= -1
    } else if (direction === 'bottom') {
      anchor.x = 0
      anchor.y = 0
    } else if (direction === 'top') {
      anchor.x = 0
    }
    return anchor
  },

  disconnectFromDataLayer: function (datalayer) {
    this.options.icon.datalayer = null
    U.FeatureMixin.disconnectFromDataLayer.call(this, datalayer)
  },

  _getIconUrl: function (name) {
    if (typeof name === 'undefined') name = 'icon'
    return this.getOption(`${name}Url`)
  },

  getIconClass: function () {
    return this.getOption('iconClass')
  },

  getIcon: function () {
    const Class = U.Icon[this.getIconClass()] || U.Icon.Default
    return new Class(this.map, { feature: this })
  },

  getCenter: function () {
    return this._latlng
  },

  getClassName: () => 'marker',

  getShapeOptions: () => [
    'properties._umap_options.color',
    'properties._umap_options.iconClass',
    'properties._umap_options.iconUrl',
    'properties._umap_options.iconOpacity',
  ],

  getAdvancedOptions: () => ['properties._umap_options.zoomTo'],

  appendEditFieldsets: function (container) {
    U.FeatureMixin.appendEditFieldsets.call(this, container)
    const coordinatesOptions = [
      ['_latlng.lat', { handler: 'FloatInput', label: L._('Latitude') }],
      ['_latlng.lng', { handler: 'FloatInput', label: L._('Longitude') }],
    ]
    const builder = new U.FormBuilder(this, coordinatesOptions, {
      callback: function () {
        if (!this._latlng.isValid()) {
          U.Alert.error(L._('Invalid latitude or longitude'))
          builder.resetField('_latlng.lat')
          builder.resetField('_latlng.lng')
        }
        this.zoomTo({ easing: false })
      },
      callbackContext: this,
    })
    const fieldset = L.DomUtil.createFieldset(container, L._('Coordinates'))
    fieldset.appendChild(builder.build())
  },

  zoomTo: function (e) {
    if (this.datalayer.isClustered() && !this._icon) {
      // callback is mandatory for zoomToShowLayer
      this.datalayer.layer.zoomToShowLayer(this, e.callback || (() => {}))
    } else {
      U.FeatureMixin.zoomTo.call(this, e)
    }
  },

  isOnScreen: function (bounds) {
    bounds = bounds || this.map.getBounds()
    return bounds.contains(this._latlng)
  },

  getPopupToolbarAnchor: function () {
    return this.options.icon.options.popupAnchor
  },
})

U.PathMixin = {
  hasGeom: function () {
    return !this.isEmpty()
  },

  connectToDataLayer: function (datalayer) {
    U.FeatureMixin.connectToDataLayer.call(this, datalayer)
    // We keep markers on their own layer on top of the paths.
    this.options.pane = this.datalayer.pane
  },

  edit: function (e) {
    if (this.map.editEnabled) {
      if (!this.editEnabled()) this.enableEdit()
      U.FeatureMixin.edit.call(this, e)
    }
  },

  _toggleEditing: function (e) {
    if (this.map.editEnabled) {
      if (this.editEnabled()) {
        this.endEdit()
        this.map.editPanel.close()
      } else {
        this.edit(e)
      }
    }
    // FIXME: disable when disabling global edit
    L.DomEvent.stop(e)
  },

  styleOptions: [
    'smoothFactor',
    'color',
    'opacity',
    'stroke',
    'weight',
    'fill',
    'fillColor',
    'fillOpacity',
    'dashArray',
    'interactive',
  ],

  getShapeOptions: () => [
    'properties._umap_options.color',
    'properties._umap_options.opacity',
    'properties._umap_options.weight',
  ],

  getAdvancedOptions: () => [
    'properties._umap_options.smoothFactor',
    'properties._umap_options.dashArray',
    'properties._umap_options.zoomTo',
  ],

  setStyle: function (options) {
    options = options || {}
    let option
    for (const idx in this.styleOptions) {
      option = this.styleOptions[idx]
      options[option] = this.getDynamicOption(option)
    }
    if (options.interactive) this.options.pointerEvents = 'visiblePainted'
    else this.options.pointerEvents = 'stroke'
    this.parentClass.prototype.setStyle.call(this, options)
  },

  _redraw: function () {
    if (this.datalayer?.isVisible()) {
      this.setStyle()
      this.resetTooltip()
    }
  },

  onAdd: function (map) {
    this._container = null
    this.setStyle()
    // Show tooltip again when Leaflet.label allow static label on path.
    // cf https://github.com/Leaflet/Leaflet/pull/3952
    // this.map.on('showmeasure', this.showMeasureTooltip, this);
    // this.map.on('hidemeasure', this.removeTooltip, this);
    this.parentClass.prototype.onAdd.call(this, map)
    if (this.editing?.enabled()) this.editing.addHooks()
    this.resetTooltip()
  },

  onRemove: function (map) {
    // this.map.off('showmeasure', this.showMeasureTooltip, this);
    // this.map.off('hidemeasure', this.removeTooltip, this);
    if (this.editing?.enabled()) this.editing.removeHooks()
    U.FeatureMixin.onRemove.call(this, map)
  },

  getBestZoom: function () {
    return this.getOption('zoomTo') || this.map.getBoundsZoom(this.getBounds(), true)
  },

  endEdit: function () {
    this.disableEdit()
    U.FeatureMixin.endEdit.call(this)
  },

  highlightPath: function () {
    this.parentClass.prototype.setStyle.call(this, {
      fillOpacity: Math.sqrt(this.getDynamicOption('fillOpacity', 1.0)),
      opacity: 1.0,
      weight: 1.3 * this.getDynamicOption('weight'),
    })
  },

  _onMouseOver: function () {
    if (this.map.measureTools?.enabled()) {
      this.map.tooltip.open({ content: this.getMeasure(), anchor: this })
    } else if (this.map.editEnabled && !this.map.editedFeature) {
      this.map.tooltip.open({ content: L._('Click to edit'), anchor: this })
    }
  },

  addInteractions: function () {
    U.FeatureMixin.addInteractions.call(this)
    this.on('editable:disable', this.onCommit)
    this.on('mouseover', this._onMouseOver)
    this.on('edit', this.makeDirty)
    this.on('drag editable:drag', this._onDrag)
    this.on('popupopen', this.highlightPath)
    this.on('popupclose', this._redraw)
  },

  _onDrag: function () {
    if (this._tooltip) this._tooltip.setLatLng(this.getCenter())
  },

  transferShape: function (at, to) {
    const shape = this.enableEdit().deleteShapeAt(at)
    this.disableEdit()
    if (!shape) return
    to.enableEdit().appendShape(shape)
    if (!this._latlngs.length || !this._latlngs[0].length) this.del()
  },

  isolateShape: function (at) {
    if (!this.isMulti()) return
    const shape = this.enableEdit().deleteShapeAt(at)
    this.disableEdit()
    if (!shape) return
    const properties = this.cloneProperties()
    const other = new (this instanceof U.Polyline ? U.Polyline : U.Polygon)(
      this.map,
      shape,
      {
        geojson: { properties },
      }
    )
    this.datalayer.addLayer(other)
    other.edit()
    return other
  },

  getContextMenuItems: function (e) {
    let items = U.FeatureMixin.getContextMenuItems.call(this, e)
    items.push({
      text: L._('Display measure'),
      callback: function () {
        U.Alert.info(this.getMeasure())
      },
      context: this,
    })
    if (this.map.editEnabled && !this.isReadOnly() && this.isMulti()) {
      items = items.concat(this.getContextMenuMultiItems(e))
    }
    return items
  },

  getContextMenuMultiItems: function (e) {
    const items = [
      '-',
      {
        text: L._('Remove shape from the multi'),
        callback: function () {
          this.enableEdit().deleteShapeAt(e.latlng)
        },
        context: this,
      },
    ]
    const shape = this.shapeAt(e.latlng)
    if (this._latlngs.indexOf(shape) > 0) {
      items.push({
        text: L._('Make main shape'),
        callback: function () {
          this.enableEdit().deleteShape(shape)
          this.editor.prependShape(shape)
        },
        context: this,
      })
    }
    return items
  },

  getContextMenuEditItems: function (e) {
    const items = U.FeatureMixin.getContextMenuEditItems.call(this, e)
    if (
      this.map.editedFeature &&
      this.isSameClass(this.map.editedFeature) &&
      this.map.editedFeature !== this
    ) {
      items.push({
        text: L._('Transfer shape to edited feature'),
        callback: function () {
          this.transferShape(e.latlng, this.map.editedFeature)
        },
        context: this,
      })
    }
    if (this.isMulti()) {
      items.push({
        text: L._('Extract shape to separate feature'),
        callback: function () {
          this.isolateShape(e.latlng, this.map.editedFeature)
        },
        context: this,
      })
    }
    return items
  },

  getInplaceToolbarActions: function (e) {
    const items = U.FeatureMixin.getInplaceToolbarActions.call(this, e)
    if (this.isMulti()) {
      items.push(U.DeleteShapeAction)
      items.push(U.ExtractShapeFromMultiAction)
    }
    return items
  },

  isOnScreen: function (bounds) {
    bounds = bounds || this.map.getBounds()
    return bounds.overlaps(this.getBounds())
  },

  zoomTo: function (e) {
    // Use bounds instead of centroid for paths.
    e = e || {}
    const easing = e.easing !== undefined ? e.easing : this.map.getOption('easing')
    if (easing) {
      this.map.flyToBounds(this.getBounds(), this.getBestZoom())
    } else {
      this.map.fitBounds(this.getBounds(), this.getBestZoom() || this.map.getZoom())
    }
    if (e.callback) e.callback.call(this)
  },
}

U.Polyline = L.Polyline.extend({
  parentClass: L.Polyline,
  includes: [U.FeatureMixin, U.PathMixin],

  staticOptions: {
    stroke: true,
    fill: false,
    mainColor: 'color',
  },

  isSameClass: (other) => other instanceof U.Polyline,

  getClassName: () => 'polyline',

  getMeasure: function (shape) {
    const length = L.GeoUtil.lineLength(this.map, shape || this._defaultShape())
    return L.GeoUtil.readableDistance(length, this.map.measureTools.getMeasureUnit())
  },

  getContextMenuEditItems: function (e) {
    const items = U.PathMixin.getContextMenuEditItems.call(this, e)
    const vertexClicked = e.vertex
    let index
    if (!this.isMulti()) {
      items.push({
        text: L._('Transform to polygon'),
        callback: this.toPolygon,
        context: this,
      })
    }
    if (vertexClicked) {
      index = e.vertex.getIndex()
      if (index !== 0 && index !== e.vertex.getLastIndex()) {
        items.push({
          text: L._('Split line'),
          callback: e.vertex.split,
          context: e.vertex,
        })
      } else if (index === 0 || index === e.vertex.getLastIndex()) {
        items.push({
          text: this.map.help.displayLabel('CONTINUE_LINE'),
          callback: e.vertex.continue,
          context: e.vertex.continue,
        })
      }
    }
    return items
  },

  getContextMenuMultiItems: function (e) {
    const items = U.PathMixin.getContextMenuMultiItems.call(this, e)
    items.push({
      text: L._('Merge lines'),
      callback: this.mergeShapes,
      context: this,
    })
    return items
  },

  toPolygon: function () {
    const geojson = this.toGeoJSON()
    geojson.geometry.type = 'Polygon'
    geojson.geometry.coordinates = [
      U.Utils.flattenCoordinates(geojson.geometry.coordinates),
    ]

    delete geojson.id // delete the copied id, a new one will be generated.

    const polygon = this.datalayer.geojsonToFeatures(geojson)
    polygon.edit()
    this.del()
  },

  getAdvancedEditActions: function (container) {
    U.FeatureMixin.getAdvancedEditActions.call(this, container)
    L.DomUtil.createButton(
      'button umap-to-polygon',
      container,
      L._('Transform to polygon'),
      this.toPolygon,
      this
    )
  },

  _mergeShapes: function (from, to) {
    const toLeft = to[0]
    const toRight = to[to.length - 1]
    const fromLeft = from[0]
    const fromRight = from[from.length - 1]
    const l2ldistance = toLeft.distanceTo(fromLeft)
    const l2rdistance = toLeft.distanceTo(fromRight)
    const r2ldistance = toRight.distanceTo(fromLeft)
    const r2rdistance = toRight.distanceTo(fromRight)
    let toMerge
    if (l2rdistance < Math.min(l2ldistance, r2ldistance, r2rdistance)) {
      toMerge = [from, to]
    } else if (r2ldistance < Math.min(l2ldistance, l2rdistance, r2rdistance)) {
      toMerge = [to, from]
    } else if (r2rdistance < Math.min(l2ldistance, l2rdistance, r2ldistance)) {
      from.reverse()
      toMerge = [to, from]
    } else {
      from.reverse()
      toMerge = [from, to]
    }
    const a = toMerge[0]
    const b = toMerge[1]
    const p1 = this.map.latLngToContainerPoint(a[a.length - 1])
    const p2 = this.map.latLngToContainerPoint(b[0])
    const tolerance = 5 // px on screen
    if (Math.abs(p1.x - p2.x) <= tolerance && Math.abs(p1.y - p2.y) <= tolerance) {
      a.pop()
    }
    return a.concat(b)
  },

  mergeShapes: function () {
    if (!this.isMulti()) return
    const latlngs = this.getLatLngs()
    if (!latlngs.length) return
    while (latlngs.length > 1) {
      latlngs.splice(0, 2, this._mergeShapes(latlngs[1], latlngs[0]))
    }
    this.setLatLngs(latlngs[0])
    if (!this.editEnabled()) this.edit()
    this.editor.reset()
    this.isDirty = true
  },

  isMulti: function () {
    return !L.LineUtil.isFlat(this._latlngs) && this._latlngs.length > 1
  },

  getVertexActions: function (e) {
    const actions = U.FeatureMixin.getVertexActions.call(this, e)
    const index = e.vertex.getIndex()
    if (index === 0 || index === e.vertex.getLastIndex())
      actions.push(U.ContinueLineAction)
    else actions.push(U.SplitLineAction)
    return actions
  },
})

U.Polygon = L.Polygon.extend({
  parentClass: L.Polygon,
  includes: [U.FeatureMixin, U.PathMixin],
  staticOptions: {
    mainColor: 'fillColor',
  },

  isSameClass: (other) => other instanceof U.Polygon,

  getClassName: () => 'polygon',

  getShapeOptions: () => {
    const options = U.PathMixin.getShapeOptions()
    options.push(
      'properties._umap_options.stroke',
      'properties._umap_options.fill',
      'properties._umap_options.fillColor',
      'properties._umap_options.fillOpacity'
    )
    return options
  },

  getInteractionOptions: () => {
    const options = U.FeatureMixin.getInteractionOptions()
    options.push('properties._umap_options.interactive')
    return options
  },

  getMeasure: function (shape) {
    const area = L.GeoUtil.geodesicArea(shape || this._defaultShape())
    return L.GeoUtil.readableArea(area, this.map.measureTools.getMeasureUnit())
  },

  getContextMenuEditItems: function (e) {
    const items = U.PathMixin.getContextMenuEditItems.call(this, e)
    const shape = this.shapeAt(e.latlng)
    // No multi and no holes.
    if (shape && !this.isMulti() && (L.LineUtil.isFlat(shape) || shape.length === 1)) {
      items.push({
        text: L._('Transform to lines'),
        callback: this.toPolyline,
        context: this,
      })
    }
    items.push({
      text: L._('Start a hole here'),
      callback: this.startHole,
      context: this,
    })
    return items
  },

  startHole: function (e) {
    this.enableEdit().newHole(e.latlng)
  },

  toPolyline: function () {
    const geojson = this.toGeoJSON()
    delete geojson.id
    delete geojson.properties.id
    geojson.geometry.type = 'LineString'
    geojson.geometry.coordinates = U.Utils.flattenCoordinates(
      geojson.geometry.coordinates
    )
    const polyline = this.datalayer.geojsonToFeatures(geojson)
    polyline.edit()
    this.del()
  },

  getAdvancedEditActions: function (container) {
    U.FeatureMixin.getAdvancedEditActions.call(this, container)
    const toPolyline = L.DomUtil.createButton(
      'button umap-to-polyline',
      container,
      L._('Transform to lines'),
      this.toPolyline,
      this
    )
  },

  isMulti: function () {
    // Change me when Leaflet#3279 is merged.
    return (
      !L.LineUtil.isFlat(this._latlngs) &&
      !L.LineUtil.isFlat(this._latlngs[0]) &&
      this._latlngs.length > 1
    )
  },

  getInplaceToolbarActions: function (e) {
    const items = U.PathMixin.getInplaceToolbarActions.call(this, e)
    items.push(U.CreateHoleAction)
    return items
  },
})
