Source: tools/services/styles.js

import Fill from 'ol/style/Fill'
import Stroke from 'ol/style/Stroke'
import Text from 'ol/style/Text'
import Icon from 'ol/style/Icon'
import RegularShape from 'ol/style/RegularShape'
import Circle from 'ol/style/Circle'
import Style from 'ol/style/Style'

import MultiLineString from 'ol/geom/MultiLineString'
import Feature from 'ol/Feature'
import Point from 'ol/geom/Point'
import LineString from 'ol/geom/LineString'
import Polygon from 'ol/geom/Polygon'

import CircleGeom from 'ol/geom/Circle'

import * as olExtent from 'ol/extent'

import Legend from 'ol-ext/legend/Legend'

import Chart from 'ol-ext/style/Chart'
import FontSymbol from 'ol-ext/style/FontSymbol'

import { bufferExtent } from './buffer'

import castArray from 'lodash/castArray'
import cloneDeep from 'lodash/cloneDeep'
import merge from 'lodash/merge'
import templateCompiler from 'lodash/template'

import {
  lineString as TurfLineString,
} from '@turf/helpers'

import nearestPointOnLine from '@turf/nearest-point-on-line'

import { convertCoordinates } from './projections'

import { getPointResolution } from 'ol/proj'
import { getPointForGeometry } from './centroid'
import { geoJsonToGeometry } from './geojsons'

/**
 * Permet de hachurer un polygon
 * @param  {array | string} color  Couleur du paterne
 * @param  {string}         type   Type de paterne
 * @return {CanvasPattern}         Paterne des hachures
 */
export function buildOLPattern (color, type) {
  // Convertion des formats de couleurs en rgb(a)
  if (Array.isArray(color)) { // Array
    // Sans transparence si non définit
    if (color.length === 3) {
      color[3] = 1
    }
    color = 'rgba(' + color[0] + ',' + color[1] + ',' + color[2] + ',' + color[3] + ')'
  } else if (color.substr(0, 1) === '#') { // Hexa
    const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(color)
    color = 'rgb('
    color += parseInt(result[1], 16) + ','
    color += parseInt(result[2], 16) + ','
    color += parseInt(result[3], 16) + ')'
  } else if (!color.substr(0, 3) === 'rgb') { // Si non rgb(a), format invalide
    throw new Error('Invalid color for generate pattern')
  }
  // Créer un canvas de dimention 6x6 qui sera répété
  const cnv = document.createElement('canvas')
  const ctx = cnv.getContext('2d')
  cnv.width = 6
  cnv.height = 6
  ctx.fillStyle = color

  // Permet de définir le paterne en fonction du type
  switch (type) {
    case 'hatching' :
      for (let i = 0; i < 6; ++i) {
        ctx.fillRect(0, i, 1, 1)
      }
      break
    case 'hatchingInvert' :
      for (let i = 0; i < 6; ++i) {
        ctx.fillRect(i, 0, 1, 1)
      }
      break
    case 'hatchingDiag' :
      for (let i = 0; i < 6; ++i) {
        ctx.fillRect(i, i, 1, 1)
      }
      break
    case 'hatchingDiagInvert' :
      for (let i = 0; i < 6; ++i) {
        ctx.fillRect(i, 5 - i, 1, 1)
      }
      break
    case 'perforedLine' :
      for (let i = 0; i < 3; ++i) {
        ctx.fillRect(0, i, 1, 1)
      }
      break
    case 'perforedLineInvert' :
      for (let i = 0; i < 3; ++i) {
        ctx.fillRect(i, 0, 1, 1)
      }
      break
    case 'perforedLineDiag' :
      for (let i = 0; i < 3; ++i) {
        ctx.fillRect(i, i, 1, 1)
      }
      break
    case 'perforedLineDiagInvert' :
      for (let i = 0; i < 3; ++i) {
        ctx.fillRect(i, 5 - i, 1, 1)
      }
      break
    case 'square' :
      for (let i = 0; i < 6; ++i) {
        ctx.fillRect(i, 0, 1, 1)
        ctx.fillRect(0, i, 1, 1)
      }
      break
    case 'squareInvert' :
      for (let i = 0; i < 6; ++i) {
        ctx.fillRect(i, i, 1, 1)
        ctx.fillRect(i, 5 - i, 1, 1)
      }
      break
    default: // dots
      ctx.fillRect(2, 2, 2, 2)
      break
  }

  return ctx.createPattern(cnv, 'repeat')
}

// #region Nouvelle Api de Style étendable

function deepFreeze (obj) {
  // On récupère les noms des propriétés définies sur obj
  const propNames = Object.getOwnPropertyNames(obj)

  // On gèle les propriétés avant de geler l'objet
  for (const name of propNames) {
    const value = obj[name]
    obj[name] = value && typeof value === 'object'
      ? deepFreeze(value)
      : value
  }

  // On gèle l'objet initial
  return Object.freeze(obj)
}

const blueStroke = {
  color: '#3399CC',
  width: 1.25,
}
const defaultFill = {
  color: 'rgba(255,255,255,0.4)',
}

const pointFill = {
  color: 'rgba(255,255,255,0.7)',
}

const pointStroke = {
  color: 'rgba(0, 153, 255, 1)',
  width: 1.5,
}

const blue = {
  color: 'rgba(0, 153, 255, 1)',
}

const red = {
  color: 'rgba(153, 0, 0, 1)',
}

const green = {
  color: 'rgba(0, 153, 50, 1)',
}

const yellow = {
  color: 'rgba(220, 200,20, 1)',
}
const orange = {
  color: 'rgba(220, 100,20, 1)',
}

export const styleHelpers = deepFreeze(JSON.parse(JSON.stringify({
  line: { stroke: blueStroke },
  polygon: { stroke: blueStroke, fill: defaultFill },

  blue,
  red,
  green,
  yellow,
  orange,

  square: { shape: { type: 'square', stroke: pointStroke, fill: pointFill, radius: 10 } },
  triangle: { shape: { type: 'triangle', stroke: pointStroke, fill: pointFill, radius: 10 } },
  star: { shape: { type: 'star', stroke: pointStroke, fill: pointFill, radius: 10 } },
  cross: { shape: { type: 'cross', stroke: pointStroke, fill: pointFill, radius: 10 } },
  diamond: { shape: { type: 'diamond', stroke: pointStroke, fill: pointFill, radius: 10 } },
  plus: { shape: { type: 'plus', stroke: pointStroke, fill: pointFill, radius: 10 } },

  blueShapeStroke: { shape: { stroke: blue } },
  redShapeStroke: { shape: { stroke: red } },
  greenShapeStroke: { shape: { stroke: green } },
  yellowShapeStroke: { shape: { stroke: yellow } },
  orangeShapeStroke: { shape: { stroke: orange } },

  circle: { circle: { stroke: pointStroke, fill: pointFill, radius: 10 } },

  blueCircleStroke: { circle: { stroke: blue } },
  redCircleStroke: { circle: { stroke: red } },
  greenCircleStroke: { circle: { stroke: green } },
  yellowCircleStroke: { circle: { stroke: yellow } },
  orangeCircleStroke: { circle: { stroke: orange } },

})))

/**
 * Permet de mapper une font graphique avec un nom utilisable pour les glyphs
 * @param {String | Object} font the font name or a description ({ font: font_name, name: font_name, copyright: '', prefix })
 * @param {Object} glyphs a key / value list of glyph definitions. Each key is the name of the glyph, the value is an object that code the font, the caracter code, the name and a search string for the glyph. { char: the char, code: the char code (if no char), theme: a theme for search puposes, name: the symbol name, search: a search string (separated with ',') }
 * @example pour ajouter des glyphs de font awesome 5
 * addFontDef({
 *   "font":"\"Font Awesome 5 Free\"",
 *   "name":"FontAwesome5",
 *   "copyright":"SIL OFL 1.1",
 *   "prefix": "fa"
 * },{
 *   "fa-glass": "\uf000",
 *   "fa-music": "\uf001",
 *   "fa-search": "\uf002",
 *   "fa-envelope-o": "\uf003",
 * })
 *
 */
export function addFontDef (font, glyphs) {
  // FontSymbol.addDefs(font, glyphs)

  let thefont = font
  if (typeof (font) === 'string') {
    thefont = { font, name: font, copyright: '' }
  }
  if (!thefont.font || typeof (thefont.font) !== 'string') {
    console.log('bad font def')
    return
  }
  const fontname = thefont.font
  FontSymbol.defs.fonts[fontname] = thefont
  for (const i in glyphs) {
    let g = glyphs[i]
    if (typeof (g) === 'string' && (g.length === 1 || g.length === 2)) {
      g = { char: g }
    }
    FontSymbol.defs.glyphs[i] = {
      font: thefont.font,
      char: g.char || '' + String.fromCodePoint(g.code) || '',
      theme: g.theme || thefont.name,
      name: g.name || i,
      search: g.search || '',
    }
  }
}

// TODO: documenter les styleOptions
/**
 * Permet d'obtenir un style openlayers
 * en objet ol.style.Style pour OpenLayers
 * @param {Object | Array} styleOptions Definition du style sous forme d'objet
 * @return {ol.style.Style} Style OpenLayers
 */
export function createStyle (styleOptions) {
  if (!styleOptions) {
    // on a pas mit de style:
    return createSingleStyle({
      ...styleHelpers.polygon,
      circle: styleHelpers.circle.circle,
    })
  } else if (!Array.isArray(styleOptions)) {
    // on a un objet, on renvoit le style
    return createSingleStyle(styleOptions)
  } else {
    // on a un tableau, on renvoit un tableau de styles afin d'avoir un style composé
    return styleOptions.map(style => {
      if (!Array.isArray(style)) {
        return createSingleStyle(style)
      } else {
        // si on a un tableau de tableau, on merge les styles
        return createSingleStyle(merge.apply({}, cloneDeep(style)))
      }
    })
  }
}

function createSingleStyle (styleOptions) {
  const { fill, stroke, text, icon, shape, circle, replaceGeometry, arrows, fontSymbol, badge, lineBreak, ...options } = styleOptions
  // si c'est arrow on renvoi direct le style arrow qui devra être utilisé dans une fonction car dépend de la feature
  if (arrows) {
    return buildArrowsStyle(arrows)
  }

  // si c'est badge on renvoi direct le style badge qui devra être utilisé dans une fonction car dépend de la feature
  if (badge) {
    return buildBadge(badge)
  }

  // si c'est unee ligne segmentée on renvoi direct le style badge qui devra être utilisé dans une fonction car dépend de la feature
  if (lineBreak) {
    return buildLineBreakStyle(lineBreak)
  }

  // recrée le style
  // remplissage
  if (fill) {
    options.fill = buildFillStyle(fill)
  }
  // ligne
  if (stroke) {
    options.stroke = buildStrokeStyle(stroke)
  }
  // texte
  if (text) {
    options.text = buildTextStyle(text)
  }
  // pour afficher un point
  if (icon) {
    options.image = buildIconStyle(icon)
  } else if (shape) {
    if (shape.type) { // forme prédéfinie: square, triangle, star, cross, diamond, plus, charts, fontSymbole
      options.image = buildTypedShapeStyle(shape)
    } else {
      options.image = buildShapeStyle(shape)
    }
  } else if (circle) {
    options.image = buildCircleStyle(circle)
  } else if (fontSymbol) {
    options.image = buildFontSymbol(fontSymbol)
  }

  const style = new Style(options)

  if (replaceGeometry) {
    setStyleGeometry(style, replaceGeometry)
  }

  // template: '<%= type + " - " + valeur %>', // => texte utilisant un template : un seul texte
  // template: ['<%= "feature type : " + type %>','\n','<%= valeur %>'], // => équivalent openlayers ["feature type : " + type,'','\n','', valeur,''] : type au dessus de valeur en utilisant la font du style texte
  // template: [{template:'<%= valeur %>',font:'bold 13px Calibri,sans-serif'},' <%= type %>','\n',{ template:'<%= valeur %>', font:'italic 11px Calibri,sans-serif',}], // => équivaleur  [valeur,'bold 13px Calibri,sans-serif',type,'','\n','', valeur,'italic 11px Calibri,sans-serif'] : type au dessus de valeur en utilisant la font du style texte pour type et la typo italic pour valeur
  // template: 'valeur', // => on met la propriété valeur
  // template: ['type','\n','valeur'], // => on met les propriétés valeur une au dessus de l'autre
  /* template:[{
      template: '<%= "nombre: " + features.length %>'
    },
    '\n',
    {
      template: 'valeur',
      forEach: true // s'appliques a toutes les features (style de cluster)
  }] */
  if (text?.template) {
    const compiledTemplates = castArray(text.template).map(template => {
      const stringTemplate = typeof template === 'string' ? template : template.template
      const font = typeof template === 'string' ? null : (template?.font || '') // openLayers RichText: font = '' => font du style

      // Si cela ne ressemble pas a un template lodash,c'est un nom de propriété
      const isTemplate = stringTemplate.indexOf('<%') > -1
      const isCarriageReturn = stringTemplate === '\n'

      return {
        property: isTemplate ? null : stringTemplate,
        // text.templateFunction: moteur de template externe à Karteis
        compiled: isTemplate ? (text.templateFunction || templateCompiler(stringTemplate)) : null,
        isCarriageReturn,
        font,
        forEach: template.forEach,
        addCarriageReturn: template.addCarriageReturn,
      }
    })

    // options de applyStyle { feature, resolution, zoom, selected, activated, hovered, tags }
    return (styleOptions) => {
      const { feature, features, context } = styleOptions
      const properties = feature.getProperties()
      const templateContext = {
        ...properties,
        features, // les features peuvents avoir été précalculée pour ne pas utiliser celle venant du cluster (par exemple avec un filtre)
        context,
      }
      // on utilise pas un reduce car la boucle for reste le plus rapide et c'est executé souvent
      const newTexts = []

      for (let i = 0, l = compiledTemplates.length; i < l; i++) {
        const { property, compiled, font, forEach, isCarriageReturn, addCarriageReturn } = compiledTemplates[i]
        if (forEach) {
          features.forEach(f => {
            const p = f.getProperties()
            const tContext = {
              ...p,
              context,
            }

            if (isCarriageReturn) {
              newTexts.push('\n', '')
            } else if (property) {
              newTexts.push(p[property], font/*, '\n', '' */)
            } else {
              newTexts.push(compiled(tContext), font/*, '\n', '' */)
            }
            if (addCarriageReturn) {
              newTexts.push('\n', '')
            }
          })
        } else {
          if (isCarriageReturn) {
            newTexts.push('\n', '')
          } else if (property) {
            newTexts.push(properties[property], font/*, '\n', '' */)
          } else {
            newTexts.push(compiled(templateContext), font/*, '\n', '' */)
          }
          if (addCarriageReturn) {
            newTexts.push('\n', '')
          }
        }
      }

      // un seul tupple de résultat, on utilise pas richText
      const newText = newTexts.length === 2 ? newTexts[0] : newTexts// .flat()
      style.getText().setText(newText)
      return style
    }
  }

  if (typeof text?.text === 'function') {
    // options de applyStyle { feature, resolution, zoom, selected, activated, hovered, tags }
    return (styleOptions) => {
      style.getText().setText(text.text(styleOptions))
      return style
    }
  }

  return style
}

function setStyleGeometry (style, replaceGeometry) {
  style.setGeometry((feature) => {
    const featureGeomtype = feature.getGeometry()?.getType()

    // A-t-on limité le remplacement de géométrie à un type de geométrie d'origine?
    if (replaceGeometry.fromGeometryType && !replaceGeometry.fromGeometryType.includes(featureGeomtype)) {
      return feature.getGeometry()
    }
    // calcule une propriété ou enregistrer la géométrie de style alternative en fonction des options "replaceGeometry"
    const { map, ...rest } = replaceGeometry
    const alternateGeometryName = `__gmapv_style_geometry_${JSON.stringify(rest)}`

    if (feature.get(alternateGeometryName)) {
      return feature.get(alternateGeometryName)
    }

    // si la geometrie d'origine change, on recalcul la nouvelle géométrie au prochain refresh (le fait de set "alternateGeometryName" entraine un refresh)
    feature.on('change:geometry', () => {
      feature.set(alternateGeometryName, null)
    })

    // pour tout les type alternative basé sur une propriété, si elle change, on recalcule la géométrie
    feature.on('propertychange', (event) => {
      if (event.key === replaceGeometry.property) {
        feature.set(alternateGeometryName, null)
      }
    })

    // on veut remplacer la géométrie par un cercle
    if (replaceGeometry.toGeometryType === 'Circle') {
      // pas de géométrie, on ne peut pas faire de transformation en cercle
      if (!feature.getGeometry()) {
        return feature.getGeometry()
      }
      // s'assure d'avoir un point
      const point = getPointForGeometry(feature.getGeometry(), replaceGeometry.position)
      const coord = point.getCoordinates()
      const radius = typeof replaceGeometry.radius === 'string' ? feature.get(`${replaceGeometry.radius}`) : replaceGeometry.radius
      const projection = typeof replaceGeometry.projection === 'function' ? replaceGeometry.projection() : (replaceGeometry.projection || 'EPSG:3857')
      const realRadius = radius / getPointResolution(projection, 1, coord, replaceGeometry.units || 'm')
      feature.set(alternateGeometryName, new CircleGeom(coord, realRadius))
    }
    // on veut remplacer la geometrie par un point
    if (replaceGeometry.toGeometryType === 'Point') {
      // pas de géométrie, on ne peut pas faire de transformation en point a partir de celle ci..
      if (!feature.getGeometry()) {
        return feature.getGeometry()
      }
      // positionner un point a un endroit "stratégique" de la géométrie
      // par exemple: début d'une ligne ou centroid d'un polygon
      const point = getPointForGeometry(feature.getGeometry(), replaceGeometry.position)
      feature.set(alternateGeometryName, point)
    }

    // on veut utiliser une geometrie qui est dans un champ de la feature
    if (replaceGeometry.toGeometryType === 'from-property') {
      // la propriété contient soit la geometrie au format geojson, soit une geom openLayers
      const geom = feature.get(replaceGeometry.property)
      // considère que "getRevision" est une fonction si la géométrie est une geom openLayers
      feature.set(alternateGeometryName, (typeof geom.getRevision === 'function') ? geom : geoJsonToGeometry(geom))
    }

    // on veut remplacer la geometrie par une ligne de rappel
    if (replaceGeometry.toGeometryType === 'ligne-rappel') {
      // pas de géométrie, on ne peut pas faire de transformation en point a partir de celle ci..
      if (!feature.getGeometry()) {
        return feature.getGeometry()
      }

      // s'assure d'avoir un point comme départ
      const point = getPointForGeometry(feature.getGeometry(), replaceGeometry.position)

      // la propriété contient soit la geometrie au format geojson, soit une geom openLayers
      let geomRappel
      if (replaceGeometry.property) {
        geomRappel = feature.get(replaceGeometry.property)
        geomRappel = geomRappel.getRevision ? geomRappel : geoJsonToGeometry(geomRappel)
      } else if ((replaceGeometry.offsetX || replaceGeometry.offsetY) && replaceGeometry.map) {
        const originPixel = replaceGeometry.map.getPixelFromCoordinate(point.getCoordinates())
        geomRappel = new LineString([
          point.getCoordinates(),
          replaceGeometry.map.getCoordinateFromPixel([originPixel[0] + (replaceGeometry.offsetX || 0), originPixel[1] + (replaceGeometry.offsetY || 0)]),
        ])
      }
      if (!geomRappel) {
        return feature.getGeometry()
      }
      // soit la geometrie de rappel contient déjà ligne, soit un point et on construit une ligne entre les deux
      const line = ['LineString', 'MultiLineString'].includes(geomRappel.getType())
        ? geomRappel
        : new LineString([point.getCoordinates(), getPointForGeometry(geomRappel, replaceGeometry.position2).getCoordinates()])

      // on mode offset, la géométrie alternative est forcement recalculée en fonction du niveau de zoom de la carte, on ne l'enregistre pas
      if ((replaceGeometry.offsetX || replaceGeometry.offsetY)) {
        return line
      }

      feature.set(alternateGeometryName, line)
    }

    return feature.get(alternateGeometryName)
  })
}

/**
 * Crée un style de remplissage
 * @param {Object} fill options calqués sur {@link https://openlayers.org/en/latest/apidoc/module-ol_style_Fill-Fill.html OpenLayer Fill}
 * @returns {ol.style.Fill}
 */
function buildFillStyle (fill) {
  return new Fill(fill)
}

/**
 * Crée un style de ligne
 * @param {stroke} stroke options calqués sur {@link https://openlayers.org/en/latest/apidoc/module-ol_style_Stroke-Stroke.html OpenLayer Stroke}
 * @returns {ol.style.Stroke}
 */
function buildStrokeStyle (stroke) {
  return new Stroke(stroke)
}

/**
 * Crée un style de texte
 * @param {Object} literalStyle
 * @param {Object} literalStyle.stroke bordure du texte, options style de ligne calqués sur {@link buildStrokeStyle}
 * @param {Object} literalStyle.fill couleur du texte, options style de remplissage calqués sur {@link buildFillStyle}
 * @param {Object} literalStyle.backgroundStroke boite autour du texte, options style de ligne calqués sur {@link buildStrokeStyle}
 * @param {Object} literalStyle.backgroundFill surlignage du texte, options style de remplissage calqués sur {@link buildFillStyle}
 * @param {*} [literalStyle.args] reste des options calqués sur {@link https://openlayers.org/en/latest/apidoc/module-ol_style_Text-Text.html OpenLayer Text}
 * @returns {ol.style.Text}
 */
function buildTextStyle ({ fill, stroke, backgroundStroke, backgroundFill, ...text }) {
  if (fill) {
    text.fill = buildFillStyle(fill)
  }
  if (stroke) {
    text.stroke = buildStrokeStyle(stroke)
  }
  if (backgroundFill) {
    text.backgroundFill = buildFillStyle(backgroundFill)
  }
  if (backgroundStroke) {
    text.backgroundStroke = buildStrokeStyle(backgroundStroke)
  }
  const style = new Text(text)

  return style
}

/**
 * Crée un style de point basé sur une icone (image)
 * @param {Object} icon options calqués sur {@link https://openlayers.org/en/latest/apidoc/module-ol_style_Icon-Icon.html OpenLayer Icon}
 * @returns {ol.style.Icon}
 */
function buildIconStyle (icon) {
  const style = new Icon(icon)

  return style
}

/**
 * Helper pour construire un style forme
 * @param {Object} options
 * @param {'square'|'triangle'|'star'|'cross'|'diamond'|'plus'|'pie'|'pie3D'|'bar'|'donut'} [options.type="square"] Type de forme a créer, renvoi un cecle si on choisi un style non reconnu
 * @param @param {*} [options.args] restes des options pour le style choisi
 * @see {@link buildShapeStyle} pour type = square, triangle, star, cross, diamond, plus
 * @see {@link buildChartStyle} pour type = pie, pie3D, bar, donut
 * @return {ol.style.RegularShape | ol-ext.style.Chart}
 */
function buildTypedShapeStyle (options) {
  const { type = 'square', ...styles } = options

  switch (type) {
    case 'square':
      return buildShapeStyle({
        ...styles,
        points: 4,
        angle: Math.PI / 4,
      })
    case 'triangle':
      return buildShapeStyle({
        ...styles,
        points: 3,
        angle: 0,
      })
    case 'star':
      return buildShapeStyle({
        ...styles,
        points: 5,
        radius2: styles.radius / 2,
        angle: 0,
      })
    case 'cross':
      return buildShapeStyle({
        ...styles,
        points: 4,
        radius2: 0,
        angle: Math.PI / 4,
      })
    case 'diamond':
      return buildShapeStyle({
        ...styles,
        points: 4,
        angle: 0,
      })
    case 'plus':
      /* return buildShapeStyle({
        ...styles,
        points: 4,
        radius2: styles.radius > 3 ? 1.05 * Math.log2(styles.radius) : 1,
        angle: 0,
      }) */
      return buildShapeStyle({
        ...styles,
        points: 4,
        radius2: 0,
        angle: 0,
      })

    case 'pie':
    case 'pie3D':
    case 'bar':
    case 'donut':
      return buildChartStyle({ ...styles, type })
    case 'fontSymbol':
      return buildFontSymbol({ ...styles })
    default: // Retourne un cercle si on a rempli un style loufoque
      return buildCircleStyle({ radius: 10, ...styles })
  }
}

/**
 * Crée un style de point basé sur une forme
 * @param {Object} literalStyle
 * @param {Object} literalStyle.stroke options style de ligne calqués sur {@link buildStrokeStyle}
 * @param {Object} literalStyle.fill options style de remplissage calqués sur {@link buildFillStyle}
 * @param @param {*} [literalStyle.args] reste des options calqués sur {@link https://openlayers.org/en/latest/apidoc/module-ol_style_RegularShape-RegularShape.html OpenLayer RegularShape}
 * @returns {ol.style.RegularShape}
 */
function buildShapeStyle ({ fill, stroke, ...shape }) {
  if (fill) {
    shape.fill = buildFillStyle(fill)
  }
  if (stroke) {
    shape.stroke = buildStrokeStyle(stroke)
  }
  const style = new RegularShape(shape)

  return style
}

/**
 * Crée un style de point basé sur un cercle
 * @param {Object} literalStyle
 * @param {Object} literalStyle.stroke options style de ligne calqués sur {@link buildStrokeStyle}
 * @param {Object} literalStyle.fill options style de remplissage calqués sur {@link buildFillStyle}
 * @param {*} [literalStyle.args] reste des options calqués sur {@link https://openlayers.org/en/latest/apidoc/module-ol_style_Circle-CircleStyle.html OpenLayer CircleStyle}
 * @returns {ol.style.Circle}
 */
function buildCircleStyle ({ fill, stroke, ...circle }) {
  if (fill) {
    circle.fill = buildFillStyle(fill)
  }
  if (stroke) {
    circle.stroke = buildStrokeStyle(stroke)
  }
  const style = new Circle(circle)

  return style
}

/**
 * Crée un style de point basé sur un chart
 * @param {Object} literalStyle
 * @param {Object} literalStyle.stroke options style de ligne calqués sur {@link buildStrokeStyle}
 * @param {Object} literalStyle.fill options style de remplissage calqués sur {@link buildFillStyle}
 * @param {*} [literalStyle.args] reste des options calqués sur {@link http://viglino.github.io/ol-ext/doc/doc-pages/ol.style.Chart.html ol-ext Chart}
 * @returns {ol-ext.style.Chart}
 */
function buildChartStyle ({ fill, stroke, ...opts }) {
  if (fill) {
    opts.fill = new Fill({ ...fill })
  }
  if (stroke) {
    opts.stroke = new Stroke({ ...stroke })
  }
  const style = new Chart({
    type: 'pie',
    rotateWithView: true,
    ...opts,
  })

  return style
}

/**
 * Construit les traits d'une fleche
 * @param {ol.coordinates} start depuis cette position
 * @param {ol.coordinates} pos position de la fleche
 * @param {ol.coordinates} lenArrow taille de la flèche (en taille de carte)
 * @param {ol.coordinates} angArrow angle de la fleche (en radian)
 * @returns [ol.coordinates]
 */
function buildArrowCoordinates (start, pos, lenArrow, angArrow) {
  const dx = pos[0] - start[0]
  const dy = pos[1] - start[1]
  const rotation = Math.atan2(dy, dx)
  // calcul : http://math.stackexchange.com/questions/68033/calculating-edge-coordinates-of-arrowhead-pretty-basic-trigonometry
  // le signe permet de gérer si on a été de haut en bas ou bas en haut
  const sign = (rotation > 0 ? 1 : -1)
  const M = sign * Math.sqrt(Math.pow(pos[0] - start[0], 2) + Math.pow(pos[1] - start[1], 2))
  const b = Math.acos((pos[0] - start[0]) / M)
  const vRight = [(sign) * (lenArrow * Math.cos(b + Math.PI - angArrow)), (sign) * (lenArrow * Math.sin(b + Math.PI - angArrow))]
  const vLeft = [(sign) * (lenArrow * Math.cos(b + Math.PI + angArrow)), (sign) * (lenArrow * Math.sin(b + Math.PI + angArrow))]
  const coordinates = []
  coordinates.push([pos[0] + vLeft[0], pos[1] + vLeft[1]])
  coordinates.push(pos)
  coordinates.push([pos[0] + vRight[0], pos[1] + vRight[1]])
  return coordinates
}

/**
 * Permet de créer les styles de ligne avec des flèches
 *
 * @param {Object} options Options pour la création des flèches
 * @param {Object} options.stroke options style de ligne calqués sur {@link buildStrokeStyle}
 * @param {Number=} [options.lenArrow = 15] longueur de la flèche en pixels
 * @param {Number=} [options.angArrow = 60] angle de la flèche
 * @param {'betweenDistance' | 'middle' | 'end' | 'none'} [options.mode='betweenDistance'] Mode d'utilisation. between: flèches espacé d'une distance ne pixel. middle: flèches au centr des segments. end: flèches au bout de chaques segments. none: pas de flèches intermédiaires
 * @param {Number?} [options.distanceBetween = 100] mode between: distance entre les flèches
 * @param {'start' | 'end'} [options.betweenStartFrom = 'start'] mode between: on commence les distance du début ou de la fin (utile si on ajoute une flèche au bout avec arrowAtEnd)
 * @param {Boolean?} [options.hideLine=false] masque la ligne (créer uniquement les flèches)
 * @param {Boolean?} [options.arrowAtEnd=false] affiche une flèche au bout de la ligne
 * @param {Boolean?} [options.hideArrowOnSmallSegment=true] Masque les flèches plus grande  que les segments
 * @param {Number=} options.minZoom Borne haute du zoom d'affichage des flèches
 * @param {Number=} options.maxZoom Borne basse du zoom d'affichage des flèches
 * @param {ol.Feature} feature Feature pour laquelle générer les flèches
 * @param {ol.map} map instance courante de la carte
 * @returns {Array<ol.Style>} Liste des styles représentant les flèches
 */
function buildArrowsStyle ({
  stroke, lenArrow = 15, angArrow = 60, mode = 'betweenDistance', distanceBetween = 100, hideLine = false, arrowAtEnd = false,
  hideArrowOnSmallSegment = true, betweenStartFrom = 'start', minZoom, maxZoom, map, ...opts
}) {
  if (!stroke) {
    return null
  }
  const angRadian = (angArrow / 2) * (Math.PI / 180)

  const { lineDash, ...arrowStroke } = stroke
  const lineStrokeStyle = new Stroke(stroke)
  const strokeStyle = new Stroke(arrowStroke)

  const validGeometry = [LineString, MultiLineString]

  return ({ feature }) => {
    const stylesArrow = []

    const view = map.getView()
    if (!hideLine) {
      stylesArrow.push(new Style({ stroke: lineStrokeStyle }))
    }

    if (minZoom !== undefined) {
      if (view.getZoom() <= minZoom) {
        return stylesArrow
      }
    }
    if (maxZoom !== undefined) {
      if (view.getZoom() >= maxZoom) {
        return stylesArrow
      }
    }

    const geometry = feature?.getGeometry()

    // Ce style n'est compatible qu'avec des lignes
    if (validGeometry.includes(geometry.contructor)) {
      return []
    }

    // Génère les flèches pour une extent plus grande que la vue actuelle
    const mapSize = map.getSize()
    const mapExtent = view.calculateExtent(mapSize)
    const renderExtent = bufferExtent(mapExtent, 1.5)

    const lenArrows = lenArrow * view.getResolution()

    // On ne traite que des LineString à partir d'ici
    const geometries = geometry instanceof MultiLineString
      ? geometry.getLineStrings()
      : [geometry]

    const arrows = []
    const addArrow = (start, end, pos) => {
      // Si la flèche est dans l'extent de la carte, on créer le style
      if (olExtent.containsCoordinate(renderExtent, pos)) {
        const coordinates = buildArrowCoordinates(start, pos, lenArrows, angRadian)
        if (hideArrowOnSmallSegment) {
          const longSeg = Math.sqrt(Math.pow((end[0] - start[0]), 2) + Math.pow((end[1] - start[1]), 2))
          const longBranche = Math.sqrt(Math.pow((pos[0] - coordinates[0][0]), 2) + Math.pow((pos[1] - coordinates[0][1]), 2))

          if ((longBranche > (longSeg / 2))) {
            return
          }
        }
        arrows.push(coordinates)
      }
    }
    // Parcours l'ensemble des lignes de la géométrie
    geometries.forEach(lineString => {
      if (!lineString || typeof lineString.forEachSegment !== 'function') {
        return null
      }

      // Parcours tous les tronçons de la ligne
      const segments = []
      lineString.forEachSegment((start, end) => {
        segments.push({ start, end })
      })
      // ajoute la flèche au début si besoin
      if (arrowAtEnd && segments.length > 0 && mode !== 'end') {
        const coordinates = buildArrowCoordinates(segments[segments.length - 1].start, segments[segments.length - 1].end, lenArrows, angRadian)
        arrows.push(coordinates)
      }

      if (mode === 'betweenDistance') {
        let alreadyTraveledLength = 0
        let lastArrowDistance = 0
        if (betweenStartFrom === 'end') {
          segments.reverse()
        }
        segments.forEach(({ start, end }) => {
          // Récupère les coordonnées en pixel du segment actuel
          const startPixel = map.getPixelFromCoordinate(start)
          const endPixel = map.getPixelFromCoordinate(end)

          // Calcule la distance du segment actuel en pixel
          const deltaX = endPixel[0] - startPixel[0]
          const deltaY = endPixel[1] - startPixel[1]
          const segmentDistance = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2), 2)

          // On ajoute des flèches sur le segment tant qu'on est encore dessus
          while (lastArrowDistance + (distanceBetween) < alreadyTraveledLength + segmentDistance) {
            // Distance depuis le début de la ligne de la flèche à placer
            const currentArrowDistance = lastArrowDistance + distanceBetween

            // Calcule des coordonnées et de la rotation de la flèche
            const t = (currentArrowDistance - alreadyTraveledLength) / segmentDistance
            let arrowX, arrowY
            if (betweenStartFrom === 'end') {
              arrowX = end[0] - t * (end[0] - start[0])
              arrowY = end[1] - t * (end[1] - start[1])
            } else {
              arrowX = start[0] + t * (end[0] - start[0])
              arrowY = start[1] + t * (end[1] - start[1])
            }
            addArrow(start, end, [arrowX, arrowY])
            // Change la valeur de lastArrowDistance
            lastArrowDistance = currentArrowDistance
          }
          // Ajoute le segment parcouru à la distance total parcourue
          alreadyTraveledLength += segmentDistance
        })
      } else if (mode === 'middle') {
        segments.forEach(({ start, end }) => {
          const arrowX = (end[0] + start[0]) / 2
          const arrowY = (end[1] + start[1]) / 2
          addArrow(start, end, [arrowX, arrowY])
        })
      } else if (mode === 'end') {
        segments.forEach(({ start, end }) => {
          addArrow(start, end, end)
        })
      }
    })

    if (arrows.length > 0) {
      stylesArrow.push(new Style({
        geometry: new MultiLineString(arrows),
        stroke: strokeStyle,
      }))
    }

    return stylesArrow
  }
}

function calculateFontSize (text) {
  const textLength = text.toString().length
  if (textLength <= 2) {
    return 0.7
  } else if (textLength === 3) {
    return 0.5
  } else {
    return 0.3
  }
}

function buildFontSymbol ({ fill, stroke, fontSize = 'auto', ...opts }) {
  if (fill) {
    opts.fill = new Fill({ ...fill })
  }
  if (stroke) {
    opts.stroke = new Stroke({ ...stroke })
  }

  if (fontSize === 'auto' && opts.text && opts?.form !== 'none') {
    fontSize = calculateFontSize(opts.text)
  } else if (fontSize === 'auto' && opts.glyph && opts?.form !== 'none') {
    fontSize = 0.7
  } else if (fontSize === 'auto') {
    fontSize = 1
  }

  const style = new FontSymbol({
    ...opts,
    fontSize,
  })

  return style
}

/** renvoit une fonction qui construit le style badge en fonction de la feature */
function buildBadge ({
  position = null, // auto
  fontSize = 'auto',
  form = 'circle', // none|circle|poi|bubble|marker|coma|shield|blazon|bookmark|hexagon|diamond|triangle|sign|ban|lozenge|square a form that will enclose the glyph, default none
  color = 'black',
  radius = 12,
  offset = 0,
  property,
  fill = { color: '#FFF' },
  stroke = { color: 'red', width: 2 },
  displacement: styleDisplacement = [0, 0],
  ...opts
}) {
  return ({ feature }) => {
    // les propriétés peuvent être des fonctions qui s'exécute sur la feature
    let _position = typeof position === 'function' ? position(feature) : position

    _position = !_position ? feature.getGeometry().getType() === 'Point' ? 'top-right' : 'center' : _position

    let _fontSize = typeof fontSize === 'function' ? fontSize(feature) : fontSize
    const _radius = typeof radius === 'function' ? radius(feature) : radius
    const _form = typeof form === 'function' ? form(feature) : form
    const _property = typeof property === 'function' ? property(feature) : property
    const _offset = typeof offset === 'function' ? offset(feature) : offset
    const _fill = typeof fill === 'function' ? fill(feature) : fill
    const _stroke = typeof stroke === 'function' ? stroke(feature) : stroke
    const _color = typeof color === 'function' ? color(feature) : color

    const [xDisplacement, yDisplacement] = styleDisplacement
    const posY = _position === 'center' ? offset : (_offset + radius - Math.floor(radius / 10)) * (_position.includes('bottom') ? -1 : 1)
    const posX = _position === 'center' ? offset : (_offset + radius - Math.floor(radius / 10)) * (_position.includes('left') ? -1 : 1)
    const displacement = [posX + xDisplacement, posY + yDisplacement]

    if (_property) {
      const propertieValue = feature.get(_property)
      opts.text = Array.isArray(propertieValue) ? propertieValue.length : `${propertieValue}`
    }

    if (_fontSize === 'auto' && opts.text) {
      _fontSize = _fontSize = calculateFontSize(opts.text)
    }
    // pour un "glyph" on considre qu'on a un seul caractère
    if (_fontSize === 'auto' && opts.glyph) {
      _fontSize = _fontSize = calculateFontSize('0')
    }

    if (_fill) {
      opts.fill = new Fill({ ..._fill })
    }
    if (_stroke) {
      opts.stroke = new Stroke({ ..._stroke })
    }

    const style = new Style({
      image: new FontSymbol({
        displacement,
        ...opts,
        fontSize: _fontSize,
        form: _form,
        radius: _radius,
        color: _color,
      }),
    })

    setStyleGeometry(style, {
      fromGeometryType: [feature.getGeometry().getType()],
      toGeometryType: 'Point',
      position: 'centroid',
    })
    return style
  }
}

/**
 * Créer le style de ligne brisée
 * @param {Object} options
 * @param {Array<ol.style.stroke>} options.breakStyles styles a appliquer dans l'ordre des coupures
 * @param {String} options.breakPointProperty nom de la proprieté ou trouver les infos de coupure
 * @param {('point' | 'distance')} [options.breakType='point'] type de coupure (point uniquement pour l'instant)
 * @param {String} options.breakPointProjection projection des points, sinon on utilise la projection de la carte
 * @param {ol.map} options.map // instance actuelle de la carte
 * @returns function
 */
function buildLineBreakStyle ({ breakStyles = [], breakPointProperty, breakType = 'point', breakPointProjection, map }) {
  // renvoi un tableau de style avec pour chaques une geomProperty
  // feature.properties[breakPointProperty] = [x,y] || [[x,y]] || [len1,len2]
  // divise la feature a chaque breakPointProperties
  // applique le breakStyles[0] à la ligneCoupée[0], si breakStyle == null => omet ce segment
  // applique le breakStyles[1] à la ligneCoupée[1]
  // >
  const validGeometry = [LineString, MultiLineString]

  const styles = breakStyles.map(style => style ? new Stroke(style.stroke) : null)

  return ({ feature }) => {
    const featureStyles = []
    const mapProjection = map.getView().getProjection().getCode()
    breakPointProjection = breakPointProjection || mapProjection
    const geometry = feature?.getGeometry()
    // Ce style n'est compatible qu'avec des lignes
    if (validGeometry.includes(geometry.contructor)) {
      return []
    }
    // On ne traite que des LineString à partir d'ici
    const geometries = geometry instanceof MultiLineString
      ? geometry.getLineStrings()
      : [geometry]

    let breakPoints = feature.get(breakPointProperty)
    breakPoints = breakPoints ? castArray(breakPoints) : []

    // pas de breakpoint, on renvoi le premier style
    if (breakPoints.length === 0) {
      return styles[0]
        ? new Style({
          stroke: styles[0],
        })
        : null
    }
    if (breakType === 'point' && !Array.isArray(breakPoints[0])) {
      // TODO
      breakPoints = [breakPoints]
    }
    // Parcours l'ensemble des lignes de la géométrie
    geometries.forEach(lineString => {
      if (!lineString) {
        return null
      }

      // turf fonctionne en wgs84
      const turfLineString = new TurfLineString(convertCoordinates(lineString.getCoordinates(), mapProjection, 'EPSG:4326'))
      const breakPointsOnLine = breakPoints
        .map(breakPoint => nearestPointOnLine(turfLineString, convertCoordinates(breakPoint, breakPointProjection, 'EPSG:4326')))
        .sort((a, b) => a.properties.location - b.properties.location)

      let segments = []
      if (breakType === 'point') {
        segments = Array.from(Array(breakPoints.length + 1)).map(() => ([]))
        // prochain break, la fonction nearestPointOnLine renvoit dans les propriétés du point l'index du segment sur lequel on était
        let breakPointIndex = 0
        let nextNearestPoint = breakPointsOnLine[breakPointIndex]
        const coords = turfLineString.geometry.coordinates

        function setNextBreakPoint (coordIndex) {
          // on arrive a un point de coupure, on termine le segment avec ce breakPoint
          segments[breakPointIndex].push(nextNearestPoint.geometry.coordinates)

          // puis on commence le segment suivant avec ce même breakPoint
          breakPointIndex++
          segments[breakPointIndex].push(nextNearestPoint.geometry.coordinates)

          if (breakPoints.length <= breakPointIndex) {
            // plus breakPoint... on mets la coordonnées de fin du segment en cours
            segments[breakPointIndex].push(coords[coordIndex])
            nextNearestPoint = null
            return
          }

          // sinon calcule le breakPointSuivant
          // const nextBreakPoint = convertCoordinates(breakPoints[breakPointIndex], breakPointProjection, 'EPSG:4326')
          nextNearestPoint = nearestPointOnLine(turfLineString, breakPointsOnLine[breakPointIndex])

          // le prochain breakpoint est sur le même segment, on termine le segment et passe au suivant
          if (nextNearestPoint.properties.index + 1 === coordIndex) {
            setNextBreakPoint(coordIndex)
          }
        }

        // on parcours les coordonnées pour les replacer dans les bon futurs segments
        coords.forEach((coord, index) => {
          if (!nextNearestPoint || index < nextNearestPoint.properties.index + 1) {
            segments[breakPointIndex].push(coord)
          } else {
            setNextBreakPoint(index)
          }
        })
      } else if (breakType === 'distance') {
        // TODO couper la ligne en fonction d'un tableau de distance (idée si besoin)
      }

      segments.forEach((coordinates, index) => {
        if (!styles?.[index] || coordinates.length === 0) {
          return
        }
        // on a utilisé turf en wgs84, on remet les coordonnées dont on a besoin dans la bonne projection
        featureStyles.push(new Style({
          geometry: new LineString(convertCoordinates(coordinates, 'EPSG:4326', mapProjection)),
          stroke: styles[index],
        }))
      })
    })
    return featureStyles.reverse()
  }
}

// #endregion

function getLegendImage (geometryType, styleOptions, resolution, canvas, offsetY) {
  // créer une feature avec le bon type et les propriétés
  const geometry = geometryType === 'Point'
    ? new Point([0, 0])
    : geometryType === 'LineString'
      ? new LineString([0, 0])
      : geometryType === 'Polygon'
        ? new Polygon([0, 0])
        : null
  const feature = new Feature(geometry)

  // on calcule l'imagine sur maxZoom car il est inclusif
  // const resolution = this.viewer.getMap().getView().getResolutionForZoom(zoom)
  // getLegendImage n'utilise pas la résolution, on l'utilise..
  let style = castArray(typeof (styleOptions) === 'function' ? styleOptions(feature, resolution, { bypassCluster: true }) : createStyle(styleOptions) || [])

  // exclu les styles ou on devrait modifier la géométrie..
  style = style.filter(({ usePointOnGeometryType }) => !usePointOnGeometryType)
  const size = canvas ? [canvas.width, canvas.height] : undefined
  canvas = Legend.getLegendImage({
    feature,
    style,
    size,
    margin: 0,
  }, canvas, offsetY)
  return canvas.toDataURL()
}

export const Styles = {
  buildOLPattern,
  styleHelpers,
  addFontDef,
  createStyle,
  getLegendImage,
}