392 lines
12 KiB
JavaScript
392 lines
12 KiB
JavaScript
/* Copyright (c) 2015 Jean-Marc VIGLINO,
|
|
released under the CeCILL-B license (French BSD license)
|
|
(http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt).
|
|
*
|
|
* Photo style for vector features
|
|
*/
|
|
import ol_style_RegularShape from 'ol/style/RegularShape.js'
|
|
import {asString as ol_color_asString} from 'ol/color.js'
|
|
import ol_style_Stroke from 'ol/style/Stroke.js'
|
|
import ol_style_Fill from 'ol/style/Fill.js'
|
|
|
|
/**
|
|
* @classdesc
|
|
* Set Photo style for vector features.
|
|
*
|
|
* @constructor
|
|
* @param {} options
|
|
* @param { default | square | circle | anchored | folio } options.kind
|
|
* @param {boolean} options.crop crop within square, default is false
|
|
* @param {Number} options.radius symbol size
|
|
* @param {Number} [options.shadow=0] drop a shadow (the shadow width in pixel)
|
|
* @param {string} [options.declutterMode] Declutter mode "declutter" | "obstacle" | "none" | undefined
|
|
* @param {ol_style_Stroke} options.stroke
|
|
* @param {String} options.src image src
|
|
* @param {String} options.crossOrigin The crossOrigin attribute for loaded images. Note that you must provide a crossOrigin value if you want to access pixel data with the Canvas renderer.
|
|
* @param {Array<number>} [options.displacement] to use with ol > 6
|
|
* @param {number} [options.offsetX=0] Horizontal offset in pixels, deprecated use displacement with ol>6
|
|
* @param {number} [options.offsetY=0] Vertical offset in pixels, deprecated use displacement with ol>6
|
|
* @param {function} [options.onload] callback when image is loaded (to redraw the layer)
|
|
* @param {function} [options.onerror] callback when image is on error (not loaded)
|
|
* @extends {ol_style_RegularShape}
|
|
* @implements {ol.structs.IHasChecksum}
|
|
* @api
|
|
*/
|
|
var ol_style_Photo = class olstylePhoto extends ol_style_RegularShape {
|
|
constructor(options) {
|
|
options = options || {}
|
|
if (!options.displacement){
|
|
options.displacement = [options.offsetX || 0, -options.offsetY || 0]
|
|
}
|
|
var sanchor = (options.kind === "anchored" ? 8 : 0)
|
|
var shadow = (Number(options.shadow) || 0)
|
|
if (!options.stroke) {
|
|
options.stroke = new ol_style_Stroke({ width: 0, color: "#000" })
|
|
}
|
|
var strokeWidth = options.stroke.getWidth()
|
|
if (strokeWidth < 0) strokeWidth = 0;
|
|
if (options.kind == 'folio') strokeWidth += 6;
|
|
options.stroke.setWidth(strokeWidth)
|
|
|
|
super({
|
|
radius: options.radius + strokeWidth + sanchor / 2 + shadow / 2,
|
|
points: 0,
|
|
displacement: [options.displacement[0] || 0, (options.displacement[1] || 0) + sanchor],
|
|
// No fill to create a hit detection Image (v5) or transparent (v6)
|
|
fill: ol_style_RegularShape.prototype.render ? new ol_style_Fill({ color: [0, 0, 0, 0] }) : null,
|
|
declutterMode: options.declutterMode,
|
|
})
|
|
this.sanchor_ = sanchor;
|
|
this._shadow = shadow;
|
|
|
|
// Hack to get the hit detection Image (v4.6.5 ?)
|
|
if (!this.getHitDetectionImage) {
|
|
var img = super.getImage.call(this)
|
|
if (!this.hitDetectionCanvas_) {
|
|
for (var i in this) {
|
|
if (this[i] && this[i].getContext && this[i] !== img) {
|
|
this.hitDetectionCanvas_ = this[i]
|
|
break
|
|
}
|
|
}
|
|
}
|
|
// Clone canvas for hit detection (old versions)
|
|
this.hitDetectionCanvas_ = document.createElement('canvas')
|
|
this.hitDetectionCanvas_.width = img.width
|
|
this.hitDetectionCanvas_.height = img.height
|
|
var hit = this.hitDetectionCanvas_
|
|
this.getHitDetectionImage = function () {
|
|
return hit
|
|
}
|
|
}
|
|
|
|
this._stroke = options.stroke
|
|
this._fill = options.fill
|
|
this._crop = options.crop
|
|
this._crossOrigin = options.crossOrigin
|
|
this._kind = options.kind || "default"
|
|
|
|
this._radius = options.radius
|
|
this._src = options.src
|
|
|
|
this._offset = [options.offsetX ? options.offsetX : 0, options.offsetY ? options.offsetY : 0]
|
|
|
|
this._onload = options.onload
|
|
this._onerror = options.onerror
|
|
|
|
if (typeof (options.opacity) == 'number'){
|
|
this.setOpacity(options.opacity)
|
|
}
|
|
if (typeof (options.rotation) == 'number'){
|
|
this.setRotation(options.rotation)
|
|
}
|
|
|
|
// Calculate image
|
|
this.render();
|
|
this.getImage()
|
|
}
|
|
/** Set photo offset
|
|
* @param {ol.pixel} offset
|
|
*/
|
|
setOffset(offset) {
|
|
this._offset = [offset[0] || 0, offset[1] || 0]
|
|
this.render()
|
|
this.getImage()
|
|
}
|
|
/**
|
|
* Clones the style.
|
|
* @return {ol_style_Photo}
|
|
*/
|
|
clone() {
|
|
var i = new ol_style_Photo({
|
|
stroke: this._stroke,
|
|
fill: this._fill,
|
|
shadow: this._shadow,
|
|
crop: this._crop,
|
|
crossOrigin: this._crossOrigin,
|
|
kind: this._kind,
|
|
radius: this._radius,
|
|
src: this._src,
|
|
offsetX: this._offset[0],
|
|
offsetY: this._offset[1],
|
|
opacity: this.getOpacity(),
|
|
rotation: this.getRotation(),
|
|
declutterMode: this.getDeclutterMode ? this.getDeclutterMode() : null,
|
|
})
|
|
i.render()
|
|
i.getImage()
|
|
return i
|
|
}
|
|
/**
|
|
* Draw the form without the image
|
|
* @private
|
|
*/
|
|
drawBack_(context, color, strokeWidth, pixelratio) {
|
|
var shadow = this._shadow
|
|
var canvas = context.canvas
|
|
context.beginPath()
|
|
context.fillStyle = color
|
|
context.clearRect(0, 0, canvas.width, canvas.height)
|
|
var width = canvas.width / pixelratio
|
|
var height = canvas.height / pixelratio
|
|
switch (this._kind) {
|
|
case 'square': {
|
|
context.rect(0, 0, width - shadow, height - shadow)
|
|
break
|
|
}
|
|
case 'circle': {
|
|
context.arc(this._radius + strokeWidth, this._radius + strokeWidth, this._radius + strokeWidth, 0, 2 * Math.PI, false)
|
|
break
|
|
}
|
|
case 'folio': {
|
|
var offset = 6
|
|
strokeWidth -= offset
|
|
context.strokeStyle = 'rgba(0,0,0,0.5)'
|
|
context.lineWidth = 1
|
|
var w = width - shadow - 2 * offset
|
|
var a = Math.atan(6 / w)
|
|
context.save()
|
|
context.rotate(-a)
|
|
context.translate(-6, 2)
|
|
context.beginPath()
|
|
context.rect(offset, offset, w, w)
|
|
context.stroke()
|
|
context.fill()
|
|
context.restore()
|
|
context.save()
|
|
context.translate(6, -1)
|
|
context.rotate(a)
|
|
context.beginPath()
|
|
context.rect(offset, offset, w, w)
|
|
context.stroke()
|
|
context.fill()
|
|
context.restore()
|
|
context.beginPath()
|
|
context.rect(offset, offset, w, w)
|
|
context.stroke()
|
|
break
|
|
}
|
|
case 'anchored': {
|
|
context.roundRect(this.sanchor_ / 2, 0, width - this.sanchor_ - shadow, height - this.sanchor_ - shadow, strokeWidth)
|
|
context.moveTo(width / 2 - this.sanchor_ - shadow / 2, height - this.sanchor_ - shadow)
|
|
context.lineTo(width / 2 + this.sanchor_ - shadow / 2, height - this.sanchor_ - shadow)
|
|
context.lineTo(width / 2 - shadow / 2, height - shadow); break
|
|
}
|
|
default: {
|
|
// roundrect
|
|
context.roundRect(0, 0, width - shadow, height - shadow, strokeWidth)
|
|
break
|
|
}
|
|
}
|
|
context.closePath()
|
|
}
|
|
/**
|
|
* @return {RenderOptions} The render options
|
|
*/
|
|
createRenderOptions() {
|
|
var opt = super.createRenderOptions()
|
|
opt.photoOptions = [
|
|
'photo',
|
|
this._crossOrigin,
|
|
this._crop,
|
|
this._src,
|
|
this._shadow,
|
|
this._kind,
|
|
].join('-')
|
|
return opt;
|
|
}
|
|
/**
|
|
* Get the image icon.
|
|
* @param {number} pixelRatio Pixel ratio.
|
|
* @return {HTMLCanvasElement} Image or Canvas element.
|
|
* @api
|
|
*/
|
|
getImage(pixelratio) {
|
|
pixelratio = pixelratio || window.devicePixelRatio;
|
|
var canvas = super.getImage(pixelratio)
|
|
if ((this._gethit || this.img_) && this._currentRatio === pixelratio) return canvas;
|
|
// Calculate image at pixel ratio
|
|
this._currentRatio = pixelratio;
|
|
|
|
var strokeStyle
|
|
var strokeWidth = 0
|
|
if (this._stroke) {
|
|
strokeStyle = ol_color_asString(this._stroke.getColor())
|
|
strokeWidth = this._stroke.getWidth()
|
|
}
|
|
|
|
// Draw hitdetection image
|
|
this._gethit = true
|
|
var context = this.getHitDetectionImage().getContext('2d')
|
|
context.save()
|
|
context.setTransform(1, 0, 0, 1, 0, 0)
|
|
this.drawBack_(context, "#000", strokeWidth, 1)
|
|
context.fill()
|
|
context.restore()
|
|
this._gethit = false
|
|
|
|
// Draw the image
|
|
context = canvas.getContext('2d')
|
|
context.save()
|
|
context.setTransform(pixelratio, 0, 0, pixelratio, 0, 0)
|
|
this.drawBack_(context, strokeStyle, strokeWidth, pixelratio)
|
|
|
|
// Draw a shadow
|
|
if (this._shadow) {
|
|
context.shadowColor = 'rgba(0,0,0,0.5)'
|
|
context.shadowBlur = pixelratio * this._shadow / 2
|
|
context.shadowOffsetX = pixelratio * this._shadow / 2
|
|
context.shadowOffsetY = pixelratio * this._shadow / 2
|
|
}
|
|
context.fill()
|
|
context.restore()
|
|
|
|
var self = this
|
|
var img = this.img_ = new Image()
|
|
if (this._crossOrigin) img.crossOrigin = this._crossOrigin
|
|
img.src = this._src
|
|
|
|
// Draw image
|
|
if (img.width) {
|
|
self.drawImage_(canvas, img, pixelratio)
|
|
} else {
|
|
img.onload = function () {
|
|
self.drawImage_(canvas, img, pixelratio)
|
|
// Force change (?!)
|
|
// self.setScale(1);
|
|
if (self._onload) self._onload()
|
|
}
|
|
if (self._onerror) {
|
|
img.onerror = function () {
|
|
self._onerror()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set anchor (ol < 6)
|
|
if (!this.getDisplacement) {
|
|
var a = this.getAnchor()
|
|
a[0] = (canvas.width / pixelratio - this._shadow) / 2 - this._offset[0]
|
|
if (this.sanchor_) {
|
|
a[1] = canvas.height / pixelratio - this._shadow - this._offset[1]
|
|
} else {
|
|
a[1] = (canvas.height / pixelratio - this._shadow) / 2 - this._offset[1]
|
|
}
|
|
}
|
|
|
|
return canvas
|
|
}
|
|
/** Returns the photo image
|
|
* @returns {HTMLImageElement}
|
|
*/
|
|
getPhoto() {
|
|
return this.img_
|
|
}
|
|
/**
|
|
* Draw an timage when loaded
|
|
* @private
|
|
*/
|
|
drawImage_(canvas, img, pixelratio) {
|
|
// Remove the circle on the canvas
|
|
var context = (canvas.getContext('2d'))
|
|
|
|
var strokeWidth = 0
|
|
if (this._stroke)
|
|
strokeWidth = this._stroke.getWidth()
|
|
var size = 2 * this._radius
|
|
|
|
context.save()
|
|
if (ol_style_RegularShape.prototype.render)
|
|
context.setTransform(pixelratio, 0, 0, pixelratio, 0, 0)
|
|
if (this._kind == 'circle') {
|
|
context.beginPath()
|
|
context.arc(this._radius + strokeWidth, this._radius + strokeWidth, this._radius, 0, 2 * Math.PI, false)
|
|
context.clip()
|
|
}
|
|
var s, x, y, w, h, sx, sy, sw, sh
|
|
// Crop the image to a square vignette
|
|
if (this._crop) {
|
|
s = Math.min(img.width / size, img.height / size)
|
|
sw = sh = s * size
|
|
sx = (img.width - sw) / 2
|
|
sy = (img.height - sh) / 2
|
|
|
|
x = y = 0
|
|
w = h = size + 1
|
|
} else {
|
|
// Fit the image to the size
|
|
s = Math.min(size / img.width, size / img.height)
|
|
sx = sy = 0
|
|
sw = img.width
|
|
sh = img.height
|
|
|
|
w = s * sw
|
|
h = s * sh
|
|
x = (size - w) / 2
|
|
y = (size - h) / 2
|
|
}
|
|
x += strokeWidth + this.sanchor_ / 2
|
|
y += strokeWidth
|
|
|
|
context.drawImage(img, sx, sy, sw, sh, x, y, w, h)
|
|
|
|
// Draw a circle to avoid aliasing on clip
|
|
if (this._kind == 'circle' && strokeWidth) {
|
|
context.beginPath()
|
|
context.strokeStyle = ol_color_asString(this._stroke.getColor())
|
|
context.lineWidth = strokeWidth / 4
|
|
context.arc(this._radius + strokeWidth, this._radius + strokeWidth, this._radius, 0, 2 * Math.PI, false)
|
|
context.stroke()
|
|
}
|
|
context.restore()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Draws a rounded rectangle using the current state of the canvas.
|
|
* Draw a rectangle if the radius is null.
|
|
* @param {Number} x The top left x coordinate
|
|
* @param {Number} y The top left y coordinate
|
|
* @param {Number} width The width of the rectangle
|
|
* @param {Number} height The height of the rectangle
|
|
* @param {Number} radius The corner radius.
|
|
*/
|
|
CanvasRenderingContext2D.prototype.roundRect = function (x, y, w, h, r) {
|
|
if (!r) {
|
|
this.rect(x,y,w,h);
|
|
} else {
|
|
if (w < 2 * r) r = w / 2;
|
|
if (h < 2 * r) r = h / 2;
|
|
this.beginPath();
|
|
this.moveTo(x+r, y);
|
|
this.arcTo(x+w, y, x+w, y+h, r);
|
|
this.arcTo(x+w, y+h, x, y+h, r);
|
|
this.arcTo(x, y+h, x, y, r);
|
|
this.arcTo(x, y, x+w, y, r);
|
|
this.closePath();
|
|
}
|
|
return this;
|
|
};
|
|
|
|
export default ol_style_Photo
|