/* Copyright (c) 2019 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ import ol_style_Style from 'ol/style/Style.js' import {asString as ol_color_asString} from 'ol/color.js' import {asArray as ol_color_asArray} from 'ol/color.js' import {ol_coordinate_dist2d} from '../geom/GeomUtils.js' /** Flow line style * Draw LineString with a variable color / width * NB: the FlowLine style doesn't impress the hit-detection. * If you want your lines to be sectionable you have to add your own style to handle this. * (with transparent line: stroke color opacity to .1 or zero width) * @constructor * @extends {ol_style_Style} * @param {Object} options * @param {boolean} options.visible draw only the visible part of the line, default true * @param {number|function} options.width Stroke width or a function that gets a feature and the position (beetween [0,1]) and returns current width * @param {number} options.width2 Final stroke width (if width is not a function) * @param {number} options.arrow Arrow at start (-1), at end (1), at both (2), none (0), default geta * @param {ol.colorLike|function} options.color Stroke color or a function that gets a feature and the position (beetween [0,1]) and returns current color * @param {ol.colorLike} options.color2 Final sroke color if color is nor a function * @param {ol.colorLike} options.arrowColor Color of arrows, if not defined used color or color2 * @param {string} options.lineCap CanvasRenderingContext2D.lineCap 'butt' | 'round' | 'square', default 'butt' * @param {number|ol.size} options.arrowSize height and width of the arrow, default 16 * @param {boolean} [options.noOverlap=false] prevent segments overlaping * @param {number} options.offset0 offset at line start * @param {number} options.offset1 offset at line end */ var ol_style_FlowLine = class olstyleFlowLine extends ol_style_Style { constructor(options) { options = options || {} super({ stroke: options.stroke, text: options.text, zIndex: options.zIndex, geometry: options.geometry }) this.setRenderer(this._render.bind(this)) // Draw only visible this._visible = (options.visible !== false) // Width if (typeof options.width === 'function') { this._widthFn = options.width } else { this.setWidth(options.width) } this.setWidth2(options.width2) // Color if (typeof options.color === 'function') { this._colorFn = options.color } else { this.setColor(options.color) } this.setColor2(options.color2) // LineCap this.setLineCap(options.lineCap) // Arrow this.setArrow(options.arrow) this.setArrowSize(options.arrowSize) this.setArrowColor(options.arrowColor) // Offset this._offset = [0, 0] this.setOffset(options.offset0, 0) this.setOffset(options.offset1, 1) // Overlap this._noOverlap = options.noOverlap } /** Set the initial width * @param {number} width width, default 0 */ setWidth(width) { this._width = width || 0 } /** Set the final width * @param {number} width width, default 0 */ setWidth2(width) { this._width2 = width } /** Get offset at start or end * @param {number} where 0=start, 1=end * @return {number} width */ getOffset(where) { return this._offset[where] } /** Add an offset at start or end * @param {number} width * @param {number} where 0=start, 1=end */ setOffset(width, where) { width = Math.max(0, parseFloat(width)) switch (where) { case 0: { this._offset[0] = width break } case 1: { this._offset[1] = width break } } } /** Set the LineCap * @param {steing} cap LineCap (round or butt), default butt */ setLineCap(cap) { this._lineCap = (cap === 'round' ? 'round' : 'butt') } /** Get the current width at step * @param {ol.feature} feature * @param {number} step current drawing step beetween [0,1] * @return {number} */ getWidth(feature, step) { if (this._widthFn) return this._widthFn(feature, step) var w2 = (typeof (this._width2) === 'number') ? this._width2 : this._width return this._width + (w2 - this._width) * step } /** Set the initial color * @param {ol.colorLike} color */ setColor(color) { try { this._color = ol_color_asArray(color) } catch (e) { this._color = [0, 0, 0, 1] } } /** Set the final color * @param {ol.colorLike} color */ setColor2(color) { try { this._color2 = ol_color_asArray(color) } catch (e) { this._color2 = null } } /** Set the arrow color * @param {ol.colorLike} color */ setArrowColor(color) { try { this._acolor = ol_color_asString(color) } catch (e) { this._acolor = null } } /** Get the current color at step * @param {ol.feature} feature * @param {number} step current drawing step beetween [0,1] * @return {string} */ getColor(feature, step) { if (this._colorFn) return ol_color_asString(this._colorFn(feature, step)) var color = this._color var color2 = this._color2 || this._color return 'rgba(' + +Math.round(color[0] + (color2[0] - color[0]) * step) + ',' + Math.round(color[1] + (color2[1] - color[1]) * step) + ',' + Math.round(color[2] + (color2[2] - color[2]) * step) + ',' + (color[3] + (color2[3] - color[3]) * step) + ')' } /** Get arrow */ getArrow() { return this._arrow } /** Set arrow * @param {number} n -1 | 0 | 1 | 2, default: 0 */ setArrow(n) { this._arrow = parseInt(n) if (this._arrow < -1 || this._arrow > 2) this._arrow = 0 } /** getArrowSize * @return {ol.size} */ getArrowSize() { return this._arrowSize || [16, 16] } /** setArrowSize * @param {number|ol.size} size */ setArrowSize(size) { if (Array.isArray(size)) this._arrowSize = size else if (typeof (size) === 'number') this._arrowSize = [size, size] } /** drawArrow * @param {CanvasRenderingContext2D} ctx * @param {ol.coordinate} p0 * @param ol.coordinate} p1 * @param {number} width * @param {number} ratio pixelratio * @private */ drawArrow(ctx, p0, p1, width, ratio) { var asize = this.getArrowSize()[0] * ratio var l = ol_coordinate_dist2d(p0, p1) var dx = (p0[0] - p1[0]) / l var dy = (p0[1] - p1[1]) / l width = Math.max(this.getArrowSize()[1] / 2, width / 2) * ratio ctx.beginPath() ctx.moveTo(p0[0], p0[1]) ctx.lineTo(p0[0] - asize * dx + width * dy, p0[1] - asize * dy - width * dx) ctx.lineTo(p0[0] - asize * dx - width * dy, p0[1] - asize * dy + width * dx) ctx.lineTo(p0[0], p0[1]) ctx.fill() } /** Renderer function * @param {Array} geom The pixel coordinates of the geometry in GeoJSON notation * @param {ol.render.State} e The olx.render.State of the layer renderer */ _render(geom, e) { if (e.geometry.getType() === 'LineString') { var i, g, p, ctx = e.context // Get geometry used at drawing if (!this._visible) { var a = e.pixelRatio / e.resolution var cos = Math.cos(e.rotation) var sin = Math.sin(e.rotation) g = e.geometry.getCoordinates() var dx = geom[0][0] - g[0][0] * a * cos - g[0][1] * a * sin var dy = geom[0][1] - g[0][0] * a * sin + g[0][1] * a * cos geom = [] for (i = 0; p = g[i]; i++) { geom[i] = [ dx + p[0] * a * cos + p[1] * a * sin, dy + p[0] * a * sin - p[1] * a * cos, p[2] ] } } var asize = this.getArrowSize()[0] * e.pixelRatio ctx.save() // Offsets if (this.getOffset(0)) this._splitAsize(geom, this.getOffset(0) * e.pixelRatio) if (this.getOffset(1)) this._splitAsize(geom, this.getOffset(1) * e.pixelRatio, true) // Arrow 1 if (geom.length > 1 && (this.getArrow() === -1 || this.getArrow() === 2)) { p = this._splitAsize(geom, asize) if (this._acolor) ctx.fillStyle = this._acolor else ctx.fillStyle = this.getColor(e.feature, 0) this.drawArrow(ctx, p[0], p[1], this.getWidth(e.feature, 0), e.pixelRatio) } // Arrow 2 if (geom.length > 1 && this.getArrow() > 0) { p = this._splitAsize(geom, asize, true) if (this._acolor) ctx.fillStyle = this._acolor else ctx.fillStyle = this.getColor(e.feature, 1) this.drawArrow(ctx, p[0], p[1], this.getWidth(e.feature, 1), e.pixelRatio) } // Split into var geoms = this._splitInto(geom, 255, 2) var k = 0 var nb = geoms.length // Draw ctx.lineJoin = 'round' ctx.lineCap = this._lineCap || 'butt' if (geoms.length > 1) { for (k = 0; k < geoms.length; k++) { var step = k / nb g = geoms[k] ctx.lineWidth = this.getWidth(e.feature, step) * e.pixelRatio ctx.strokeStyle = this.getColor(e.feature, step) ctx.beginPath() ctx.moveTo(g[0][0], g[0][1]) for (i = 1; p = g[i]; i++) { ctx.lineTo(p[0], p[1]) } ctx.stroke() } } ctx.restore() } } /** Split extremity at * @param {ol.geom.LineString} geom * @param {number} asize * @param {boolean} end start=false or end=true, default false (start) */ _splitAsize(geom, asize, end) { var p, p1, p0 var dl, d = 0 if (end) p0 = geom.pop() else p0 = geom.shift() p = p0 while (geom.length) { if (end) p1 = geom.pop() else p1 = geom.shift() dl = ol_coordinate_dist2d(p, p1) if (d + dl > asize) { p = [p[0] + (p1[0] - p[0]) * (asize - d) / dl, p[1] + (p1[1] - p[1]) * (asize - d) / dl] dl = ol_coordinate_dist2d(p, p0) if (end) { geom.push(p1) geom.push(p) geom.push([p[0] + (p0[0] - p[0]) / dl, p[1] + (p0[1] - p[1]) / dl]) } else { geom.unshift(p1) geom.unshift(p) geom.unshift([p[0] + (p0[0] - p[0]) / dl, p[1] + (p0[1] - p[1]) / dl]) } break } d += dl p = p1 } return [p0, p] } /** Split line geometry into equal length geometries * @param {Array} geom * @param {number} nb number of resulting geometries, default 255 * @param {number} nim minimum length of the resulting geometries, default 1 */ _splitInto(geom, nb, min) { var i, p var dt = this._noOverlap ? 1 : .9 // Split geom into equal length geoms var geoms = [] var dl, l = 0 for (i = 1; p = geom[i]; i++) { l += ol_coordinate_dist2d(geom[i - 1], p) } var length = Math.max(min || 2, l / (nb || 255)) var p0 = geom[0] l = 0 var g = [p0] i = 1 p = geom[1] while (i < geom.length) { var dx = p[0] - p0[0] var dy = p[1] - p0[1] dl = Math.sqrt(dx * dx + dy * dy) if (l + dl > length) { var d = (length - l) / dl g.push([ p0[0] + dx * d, p0[1] + dy * d ]) geoms.push(g) p0 = [ p0[0] + dx * d * dt, p0[1] + dy * d * dt ] g = [p0] l = 0 } else { l += dl p0 = p g.push(p0) i++ p = geom[i] } } geoms.push(g) return geoms } } export default ol_style_FlowLine