Source: tools/services/featureSplit.js

import { unByKey } from 'ol/Observable'
import VectorLayer from 'ol/layer/Vector'
import VectorSource from 'ol/source/Vector'
import Feature from 'ol/Feature'
import { Fill, Stroke, Style } from 'ol/style'
import GeoJSON from 'ol/format/GeoJSON'

import ControlCreateTools from '../../control/ControlCreateTools'

import JSTSGeoJSON from 'jsts/org/locationtech/jts/io/GeoJSONParser'
import UnionOp from 'jsts/org/locationtech/jts/operation/union/UnionOp'
import Polygonizer from 'jsts/org/locationtech/jts/operation/polygonize/Polygonizer'
import GeometryFactory from 'jsts/org/locationtech/jts/geom/GeometryFactory'
import Coordinate from 'jsts/org/locationtech/jts/geom/Coordinate'

/**
 * Est-ce que cette feature peut-être découpée?
 * @param {Feature} feature
 * @returns est-ce que la feature peut être découpée
 */
export function isSplitPossible (feature) {
  return feature &&
    (feature.getGeometry().getType() === 'Polygon' ||
    (feature.getGeometry().getType() === 'MultiPolygon' && feature.getGeometry().getPolygons().length === 1))
}

/**
 * Lance le découpage de la feature
 * @param {Object} options
 * @param {Feature} options.feature
 * @param {Viewer} options.viewer instance active du viewer
 * @param {string} [options.toolId] Outil qui demande la découpe
 * @param {object} [options.snapOptions] options de snap (doc TODO)
 * @returns les features crées, null si aucune n'a été créé
 */
export async function splitFeature (options = {}) {
  const { feature, viewer, toolId } = options
  let { snapOptions = {} } = options
  const rtn = new Promise((resolve, reject) => {
    if (!isSplitPossible(feature)) {
      console.warn('Feature incorrecte en entrée', feature)
      return
    }

    snapOptions = {
      snapGroups: ['KMAPV'].concat(snapOptions?.snapGroups || []),
      pixelTolerance: snapOptions?.pixelTolerance || 20,
    }

    // #region style d'affichage
    const white = [255, 255, 255, 1]
    const sketchColor = [0, 153, 255, 1]
    const sketchFillColor = [0, 153, 255, 0.5]
    const validColor = [40, 180, 40, 0.5]
    const width = 3

    // le polygone d'origine
    const editStyle = [
    // polygone
      new Style({
        fill: new Fill({
          color: sketchFillColor,
        }),
      }),
      // lignes
      new Style({
        stroke: new Stroke({
          color: white,
          width: width + 2,
        }),
      }),
      new Style({
        stroke: new Stroke({
          color: sketchColor,
          width,
          lineDash: [10, 5],
        }),
      }),
    ]

    // l'affichage "valide"
    const validStyle = [
    // polygone
      new Style({
        fill: new Fill({
          color: validColor,
        }),
      }),
      // lignes
      new Style({
        stroke: new Stroke({
          color: white,
          width: width + 2,
        }),
      }),
      new Style({
        stroke: new Stroke({
          color: validColor,
          width,
          lineDash: [10, 5],
        }),
      }),
    ]

    // #endregion

    const geojsonReader = new GeoJSON()
    const jstsParser = new JSTSGeoJSON()
    // Create a GeometryFactory if you don't have one already
    const geometryFactory = new GeometryFactory()

    /** on se s'interesse qu'a la géométrie, on utilise une copie de feature  */
    const originalFeature = new Feature({ geometry: feature.getGeometry() })

    const polygonAsGeoJson = geojsonReader.writeGeometryObject(
      originalFeature.getGeometry().getType() === 'Polygon'
        ? originalFeature.getGeometry()
        : originalFeature.getGeometry().getPolygons()[0])

    // lit la géométrie ol vers jsts
    const originPolygon = jstsParser.read(polygonAsGeoJson)

    // calque temporaire des modifications
    const modifyFeatureLayer = new VectorLayer({
      [viewer.commonLayer.propertiesName.ID_LAYER]: '__SPLIT_FEATURE_LAYER',
      // zIndex: 10000,
      source: new VectorSource(),
      style: editStyle,
      displayInLayerSwitcher: false,
    })

    // variables pour faire le ménage
    const featureWasVisible = feature.get(viewer.dataLayer.propertiesName.VISIBLE_FEATURE) !== false
    const horizontalToolbarVisility = viewer.horizontalToolbar.getVisible()
    const currentMapMode = viewer.commonLayer.getCurrentMapMode()
    const events = []

    // fait le ménage
    function removeInterraction () {
      if (featureWasVisible) { viewer.dataLayer.showFeature(feature) }

      // retire les ecouteurs d'évènements nécessaire au control
      events.forEach(unByKey)

      viewer.Map.removeControl(createTool)

      // drawInteraction.setActive(false)
      // viewer.Map.removeInteraction(drawInteraction)

      viewer.getFeatureSnapper().removeSource(modifyFeatureLayer.getSource())
      viewer.commonLayer.removeLayer('__SPLIT_FEATURE_LAYER', 'SYSTEM')
      viewer.horizontalToolbar.setVisible(horizontalToolbarVisility)
      viewer.dataLayer.changeMapMode(currentMapMode.name, currentMapMode.toolId)
    }

    viewer.dataLayer.changeMapMode('_drawLineStringTouch', toolId)
    const mapMode = viewer.commonLayer.getCurrentMapMode()
    const [drawInteraction] = mapMode.interactions

    // la boite d'outils de création ne fait qu'utiliser l'interaction drawtouch venant de datalayer, on fourni la bonne
    const createToolOptions = {
      viewer,
      geometryType: 'LineString',
      interaction: drawInteraction,
      title: 'Tracer la ligne de coupe',
      digitalizeOptions: {
        snapOptions,
      },
    }

    const createTool = new ControlCreateTools(createToolOptions)

    function getExtendedPoint (coordinate1, coordinate2) {
      const ax = coordinate1[0]
      const ay = coordinate1[1]
      const bx = coordinate2[0]
      const by = coordinate2[1]
      const len = 0.5

      const lengthAB = Math.sqrt(Math.pow((ax - bx), 2) + Math.pow((ay - by), 2))
      const cx = bx + (bx - ax) / lengthAB * len
      const cy = by + (by - ay) / lengthAB * len
      return !isNaN(cx) && !isNaN(cy) ? [cx, cy] : null
    }

    // effectu la découpe a l'aide de jsts
    function cutPolygonWithLineCoordinates (coordinates) {
      coordinates = [].concat(coordinates)
      // on prolonge les coordonnées de quelques cm afin de dépasser du polygone

      const prolongementDebut = getExtendedPoint(coordinates[1], coordinates[0])
      const prolongementFin = getExtendedPoint(coordinates[coordinates.length - 2], coordinates[coordinates.length - 1])

      if (prolongementDebut) {
        coordinates.splice(0, 0, prolongementDebut)
      }
      if (prolongementFin) {
        coordinates.push(prolongementFin)
      }
      // Parse Line geometry to jsts type
      const line = geometryFactory.createLineString(coordinates.map(coordinates => new Coordinate(coordinates[0], coordinates[1])))

      // Perform union of Polygon and Line and use Polygonizer to split the polygon by line
      const union = UnionOp.union(originPolygon.getExteriorRing(), line)
      const polygonizer = new Polygonizer()

      // Splitting polygon in two part
      polygonizer.add(union)

      // Get splitted polygons (transforme les lignes en polygones)
      return polygonizer.getPolygons()
    }

    function onGeomChange (coordinates) {
      const polygons = cutPolygonWithLineCoordinates(coordinates)

      // This will execute only if polygon is successfully splitted into two parts
      if (polygons.array.length === 2) {
        // Clear old splitted polygons and measurement ovelays
        modifyFeatureLayer.getSource().clear()

        polygons.array.forEach((geom) => {
          const jsonSplittedPolygon = jstsParser.write(geom)
          const feature = geojsonReader.readFeature(jsonSplittedPolygon)
          // Add splitted polygon to vector layer
          modifyFeatureLayer.getSource().addFeature(feature)
          // Change Style of Splitted polygons
          modifyFeatureLayer.setStyle(validStyle)
        })
      } else if (polygons.array.length < 2) {
        // si on a retiré des points et qu'on a plus de découpe on revient au status de départ
        modifyFeatureLayer.setStyle(editStyle)
        modifyFeatureLayer.getSource().clear()
        // Add original polygon to vector layer if no intersection is there between line and polygon
        modifyFeatureLayer.getSource().addFeature(originalFeature)
      }
    }

    // on va d'abord ajouter un calque temporaire puis copier la feature dessus
    try {
      viewer.dataLayer.hideFeature(feature)

      // #region Calque et interraction nécessaire a cet outil
      viewer.commonLayer.addLayer(modifyFeatureLayer, 'SYSTEM')
      viewer.getFeatureSnapper().addSource({
        groups: [{ name: 'KMAPV', edge: true, vertex: true }],
        source: modifyFeatureLayer.getSource(),
      })

      modifyFeatureLayer.getSource().addFeature(originalFeature)

      viewer.horizontalToolbar.setVisible(false)

      // viewer.Map.addInteraction(drawInteraction)
      viewer.Map.addControl(createTool)

      let modifyFeatureEvent = null

      events.push(drawInteraction.on('drawabort', () => {
        // on remet en mapmode normal
        unByKey(modifyFeatureEvent)
        removeInterraction.call(this)
        resolve(null)
      }))

      events.push(drawInteraction.on('drawstart', (e) => {
        modifyFeatureEvent = e.feature.on('change', (e) => {
          if (e.target?.getGeometry()?.getCoordinates()?.length > 1) {
            onGeomChange.call(this, e.target.getGeometry().getCoordinates())
          }
        })
      }))

      events.push(drawInteraction.on('drawend', (e) => {
        // lorsque l'utilisateur termine, si on a deux polygone on
        const createdFeatures = modifyFeatureLayer.getSource().getFeatures()
        unByKey(modifyFeatureEvent)
        removeInterraction.call(this)
        resolve(createdFeatures.length === 2 ? createdFeatures : null)
      }))

      // resolve(evt)

    // #endregion
    } catch (err) {
      console.error('[splitFeature]', err)
      removeInterraction.call(this)
      reject(err)
    }
  })

  return rtn
}

export const FeatureSplit = {
  isSplitPossible,
}