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,
}