416 lines
13 KiB
JavaScript
416 lines
13 KiB
JavaScript
import ol_layer_Vector from 'ol/layer/Vector.js'
|
|
import {unByKey as ol_Observable_unByKey} from 'ol/Observable.js'
|
|
import {easeOut as ol_easing_easeOut} from 'ol/easing.js'
|
|
import ol_Object from 'ol/Object.js'
|
|
import ol_style_Style from 'ol/style/Style.js'
|
|
import ol_style_Stroke from 'ol/style/Stroke.js'
|
|
import ol_style_Fill from 'ol/style/Fill.js'
|
|
import {asString as ol_color_asString} from 'ol/color.js'
|
|
import { VERSION as ol_util_VERSION } from 'ol/util.js'
|
|
|
|
import {ol_coordinate_getIntersectionPoint} from '../geom/GeomUtils.js'
|
|
|
|
/** ol.layer.Vector.prototype.setRender3D
|
|
* @extends {ol.layer.Vector}
|
|
* @param {ol_render3D}
|
|
*/
|
|
ol_layer_Vector.prototype.setRender3D = function (r) {
|
|
r.setLayer(this);
|
|
}
|
|
|
|
/**
|
|
* @classdesc
|
|
* 3D vector layer rendering
|
|
* @constructor
|
|
* @param {Object} param
|
|
* @param {ol.layer.Vector} param.layer the layer to display in 3D
|
|
* @param {ol.style.Style} options.style drawing style
|
|
* @param {function|boolean} param.active a function that returns a boolean or a boolean ,default true
|
|
* @param {boolean} param.ghost use ghost style
|
|
* @param {number} param.maxResolution max resolution to render 3D
|
|
* @param {number} param.defaultHeight default height if none is return by a propertie
|
|
* @param {function|string|Number} param.height a height function (returns height giving a feature) or a popertie name for the height or a fixed value
|
|
*/
|
|
var ol_render3D = class olrender3D extends ol_Object {
|
|
constructor(options) {
|
|
options = options || {}
|
|
|
|
options.maxResolution = options.maxResolution || 100
|
|
options.defaultHeight = options.defaultHeight || 0
|
|
super(options)
|
|
|
|
this.setStyle(options.style)
|
|
this.set('ghost', options.ghost)
|
|
this.setActive(options.active || options.active !== false)
|
|
|
|
this.height_ = options.height = this.getHfn(options.height)
|
|
if (options.layer)
|
|
this.setLayer(options.layer)
|
|
}
|
|
/**
|
|
* Set style associated with the renderer
|
|
* @param {ol.style.Style} s
|
|
*/
|
|
setStyle(s) {
|
|
if (s instanceof ol_style_Style)
|
|
this._style = s
|
|
else
|
|
this._style = new ol_style_Style()
|
|
if (!this._style.getStroke()) {
|
|
this._style.setStroke(new ol_style_Stroke({
|
|
width: 1,
|
|
color: 'red'
|
|
}))
|
|
}
|
|
if (!this._style.getFill()) {
|
|
this._style.setFill(new ol_style_Fill({ color: 'rgba(0,0,255,0.5)' }))
|
|
}
|
|
// Get the geometry
|
|
if (s && s.getGeometry()) {
|
|
var geom = s.getGeometry()
|
|
if (typeof (geom) === 'function') {
|
|
this.set('geometry', geom)
|
|
} else {
|
|
this.set('geometry', function () { return geom })
|
|
}
|
|
} else {
|
|
this.set('geometry', function (f) { return f.getGeometry() })
|
|
}
|
|
}
|
|
/**
|
|
* Get style associated with the renderer
|
|
* @return {ol.style.Style}
|
|
*/
|
|
getStyle() {
|
|
return this._style
|
|
}
|
|
/** Set active
|
|
* @param {function|boolean} active
|
|
*/
|
|
setActive(active) {
|
|
if (typeof (active) === 'function') {
|
|
this._active = active
|
|
}
|
|
else {
|
|
this._active = function () { return active }
|
|
}
|
|
if (this.layer_)
|
|
this.layer_.changed()
|
|
}
|
|
/** Get active
|
|
* @return {boolean}
|
|
*/
|
|
getActive() {
|
|
return this._active()
|
|
}
|
|
/** Calculate 3D at potcompose
|
|
* @private
|
|
*/
|
|
onPostcompose_(e) {
|
|
if (!this.getActive())
|
|
return
|
|
var res = e.frameState.viewState.resolution
|
|
if (res > this.get('maxResolution'))
|
|
return
|
|
this.res_ = res * 400
|
|
|
|
if (this.animate_) {
|
|
var elapsed = e.frameState.time - this.animate_
|
|
if (elapsed < this.animateDuration_) {
|
|
this.elapsedRatio_ = this.easing_(elapsed / this.animateDuration_)
|
|
// tell OL3 to continue postcompose animation
|
|
e.frameState.animate = true
|
|
} else {
|
|
this.animate_ = false
|
|
this.height_ = this.toHeight_
|
|
}
|
|
}
|
|
|
|
var ratio = this._ratio = e.frameState.pixelRatio
|
|
var ctx = e.context
|
|
this.matrix_ = e.frameState.coordinateToPixelTransform
|
|
this.inversePixelTransform_ = e.inversePixelTransform;
|
|
// this.center_ = [ctx.canvas.width / 2 / ratio, ctx.canvas.height / ratio]
|
|
this.center_ = [e.frameState.size[0] / 2, e.frameState.size[1]]
|
|
|
|
var f = this.layer_.getSource().getFeaturesInExtent(e.frameState.extent)
|
|
|
|
ctx.save()
|
|
ctx.scale(ratio, ratio)
|
|
var s = this.getStyle()
|
|
ctx.lineWidth = s.getStroke().getWidth()
|
|
ctx.strokeStyle = ol_color_asString(s.getStroke().getColor())
|
|
ctx.fillStyle = ol_color_asString(s.getFill().getColor())
|
|
var builds = []
|
|
for (var i = 0; i < f.length; i++) {
|
|
var h = this.getFeatureHeight(f[i])
|
|
if (h) builds.push(this.getFeature3D_(f[i], h))
|
|
}
|
|
if (this.get('ghost')) {
|
|
this.drawGhost3D_(ctx, builds)
|
|
} else {
|
|
this.drawFeature3D_(ctx, builds)
|
|
}
|
|
ctx.restore()
|
|
}
|
|
/** Set layer to render 3D
|
|
*/
|
|
setLayer(l) {
|
|
if (this._listener) {
|
|
this._listener.forEach(function (l) {
|
|
ol_Observable_unByKey(l)
|
|
})
|
|
}
|
|
this.layer_ = l
|
|
this._listener = l.on(['postcompose', 'postrender'], this.onPostcompose_.bind(this))
|
|
}
|
|
/** Create a function that return height of a feature
|
|
* @param {function|string|number} h a height function or a popertie name or a fixed value
|
|
* @return {function} function(f) return height of the feature f
|
|
*/
|
|
getHfn(h) {
|
|
switch (typeof (h)) {
|
|
case 'function': return h
|
|
case 'string': {
|
|
var dh = this.get('defaultHeight')
|
|
return (function (f) {
|
|
return (Number(f.get(h)) || dh)
|
|
})
|
|
}
|
|
case 'number': return (function ( /*f*/) { return h })
|
|
default: return (function ( /*f*/) { return 10 })
|
|
}
|
|
}
|
|
/** Animate rendering
|
|
* @param {olx.render3D.animateOptions}
|
|
* @param {string|function|number} param.height an attribute name or a function returning height of a feature or a fixed value
|
|
* @param {number} param.duration the duration of the animatioin ms, default 1000
|
|
* @param {ol.easing} param.easing an ol easing function
|
|
* @api
|
|
*/
|
|
animate(options) {
|
|
options = options || {}
|
|
this.toHeight_ = this.getHfn(options.height)
|
|
this.animate_ = new Date().getTime()
|
|
this.animateDuration_ = options.duration || 1000
|
|
this.easing_ = options.easing || ol_easing_easeOut
|
|
// Force redraw
|
|
this.layer_.changed()
|
|
}
|
|
/** Check if animation is on
|
|
* @return {bool}
|
|
*/
|
|
animating() {
|
|
if (this.animate_ && new Date().getTime() - this.animate_ > this.animateDuration_) {
|
|
this.animate_ = false
|
|
}
|
|
return !!this.animate_
|
|
}
|
|
/** Get feature height
|
|
* @param {ol.Feature} f
|
|
*/
|
|
getFeatureHeight(f) {
|
|
if (this.animate_) {
|
|
var h1 = this.height_(f)
|
|
var h2 = this.toHeight_(f)
|
|
return (h1 * (1 - this.elapsedRatio_) + this.elapsedRatio_ * h2)
|
|
}
|
|
else
|
|
return this.height_(f)
|
|
}
|
|
/** Get elevation line
|
|
* @private
|
|
*/
|
|
hvector_(pt, h) {
|
|
var p0 = [
|
|
pt[0] * this.matrix_[0] + pt[1] * this.matrix_[1] + this.matrix_[4],
|
|
pt[0] * this.matrix_[2] + pt[1] * this.matrix_[3] + this.matrix_[5]
|
|
]
|
|
var p1 = [
|
|
p0[0] + h / this.res_ * (p0[0] - this.center_[0]),
|
|
p0[1] + h / this.res_ * (p0[1] - this.center_[1])
|
|
]
|
|
|
|
var version = parseFloat(ol_util_VERSION);
|
|
// ol@v9.1+
|
|
if (version > 9.0) {
|
|
p0 = [
|
|
p0[0] * this.inversePixelTransform_[0] - p0[1] * this.inversePixelTransform_[1] + this.inversePixelTransform_[4],
|
|
- p0[0] * this.inversePixelTransform_[2] + p0[1] * this.inversePixelTransform_[3] + this.inversePixelTransform_[5]
|
|
]
|
|
p1 = [
|
|
p1[0] * this.inversePixelTransform_[0] - p1[1] * this.inversePixelTransform_[1] + this.inversePixelTransform_[4],
|
|
- p1[0] * this.inversePixelTransform_[2] + p1[1] * this.inversePixelTransform_[3] + this.inversePixelTransform_[5]
|
|
]
|
|
return {
|
|
p0: [p0[0]/this._ratio, p0[1]/this._ratio],
|
|
p1: [p1[0]/this._ratio, p1[1]/this._ratio]
|
|
}
|
|
}
|
|
// Old versions
|
|
return {
|
|
p0: p0,
|
|
p1: p1
|
|
}
|
|
}
|
|
/** Get drawing
|
|
* @private
|
|
*/
|
|
getFeature3D_(f, h) {
|
|
var geom = this.get('geometry')(f)
|
|
var c = geom.getCoordinates()
|
|
switch (geom.getType()) {
|
|
case "Polygon":
|
|
c = [c]
|
|
// fallthrough
|
|
case "MultiPolygon":
|
|
var build = []
|
|
for (var i = 0; i < c.length; i++) {
|
|
for (var j = 0; j < c[i].length; j++) {
|
|
var b = []
|
|
for (var k = 0; k < c[i][j].length; k++) {
|
|
b.push(this.hvector_(c[i][j][k], h))
|
|
}
|
|
build.push(b)
|
|
}
|
|
}
|
|
return { type: "MultiPolygon", feature: f, geom: build, height: h }
|
|
case "Point":
|
|
return { type: "Point", feature: f, geom: this.hvector_(c, h), height: h }
|
|
default: return {}
|
|
}
|
|
}
|
|
/** Draw a feature
|
|
* @param {CanvasRenderingContext2D} ctx
|
|
* @param {ol.Feature} build
|
|
* @private
|
|
*/
|
|
drawFeature3D_(ctx, build) {
|
|
var i, j, b, k
|
|
// Construct
|
|
for (i = 0; i < build.length; i++) {
|
|
switch (build[i].type) {
|
|
case "MultiPolygon": {
|
|
for (j = 0; j < build[i].geom.length; j++) {
|
|
b = build[i].geom[j]
|
|
for (k = 0; k < b.length; k++) {
|
|
ctx.beginPath()
|
|
ctx.moveTo(b[k].p0[0], b[k].p0[1])
|
|
ctx.lineTo(b[k].p1[0], b[k].p1[1])
|
|
ctx.stroke()
|
|
}
|
|
}
|
|
break
|
|
}
|
|
case "Point": {
|
|
var g = build[i].geom
|
|
ctx.beginPath()
|
|
ctx.moveTo(g.p0[0], g.p0[1])
|
|
ctx.lineTo(g.p1[0], g.p1[1])
|
|
ctx.stroke()
|
|
break
|
|
}
|
|
default: break
|
|
}
|
|
}
|
|
// Roof
|
|
for (i = 0; i < build.length; i++) {
|
|
switch (build[i].type) {
|
|
case "MultiPolygon": {
|
|
ctx.beginPath()
|
|
for (j = 0; j < build[i].geom.length; j++) {
|
|
b = build[i].geom[j]
|
|
if (j == 0) {
|
|
ctx.moveTo(b[0].p1[0], b[0].p1[1])
|
|
for (k = 1; k < b.length; k++) {
|
|
ctx.lineTo(b[k].p1[0], b[k].p1[1])
|
|
}
|
|
} else {
|
|
ctx.moveTo(b[0].p1[0], b[0].p1[1])
|
|
for (k = b.length - 2; k >= 0; k--) {
|
|
ctx.lineTo(b[k].p1[0], b[k].p1[1])
|
|
}
|
|
}
|
|
ctx.closePath()
|
|
}
|
|
ctx.fill("evenodd")
|
|
ctx.stroke()
|
|
break
|
|
}
|
|
case "Point": {
|
|
b = build[i]
|
|
var t = b.feature.get('label')
|
|
if (t) {
|
|
var p = b.geom.p1
|
|
var f = ctx.fillStyle
|
|
ctx.fillStyle = ctx.strokeStyle
|
|
ctx.textAlign = 'center'
|
|
ctx.textBaseline = 'bottom'
|
|
ctx.fillText(t, p[0], p[1])
|
|
var m = ctx.measureText(t)
|
|
var h = Number(ctx.font.match(/\d+(\.\d+)?/g).join([]))
|
|
ctx.fillStyle = "rgba(255,255,255,0.5)"
|
|
ctx.fillRect(p[0] - m.width / 2 - 5, p[1] - h - 5, m.width + 10, h + 10)
|
|
ctx.strokeRect(p[0] - m.width / 2 - 5, p[1] - h - 5, m.width + 10, h + 10)
|
|
ctx.fillStyle = f
|
|
//console.log(build[i].feature.getProperties())
|
|
}
|
|
break
|
|
}
|
|
default: break
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* @private
|
|
*/
|
|
drawGhost3D_(ctx, build) {
|
|
var i, j, b, k
|
|
// Construct
|
|
for (i = 0; i < build.length; i++) {
|
|
switch (build[i].type) {
|
|
case "MultiPolygon": {
|
|
for (j = 0; j < build[i].geom.length; j++) {
|
|
b = build[i].geom[j]
|
|
for (k = 0; k < b.length - 1; k++) {
|
|
ctx.beginPath()
|
|
ctx.moveTo(b[k].p0[0], b[k].p0[1])
|
|
ctx.lineTo(b[k].p1[0], b[k].p1[1])
|
|
ctx.lineTo(b[k + 1].p1[0], b[k + 1].p1[1])
|
|
ctx.lineTo(b[k + 1].p0[0], b[k + 1].p0[1])
|
|
ctx.lineTo(b[k].p0[0], b[k].p0[1])
|
|
|
|
var m = [(b[k].p0[0] + b[k + 1].p0[0]) / 2, (b[k].p0[1] + b[k + 1].p0[1]) / 2]
|
|
var h = [b[k].p0[1] - b[k + 1].p0[1], -b[k].p0[0] + b[k + 1].p0[0]]
|
|
var c = ol_coordinate_getIntersectionPoint(
|
|
[m, [m[0] + h[0], m[1] + h[1]]],
|
|
[b[k].p1, b[k + 1].p1]
|
|
)
|
|
var gradient = ctx.createLinearGradient(
|
|
m[0], m[1],
|
|
c[0], c[1]
|
|
)
|
|
gradient.addColorStop(0, 'rgba(255,255,255,.2)')
|
|
gradient.addColorStop(1, 'rgba(255,255,255,0)')
|
|
ctx.fillStyle = gradient
|
|
ctx.fill()
|
|
}
|
|
}
|
|
break
|
|
}
|
|
case "Point": {
|
|
var g = build[i].geom
|
|
ctx.beginPath()
|
|
ctx.moveTo(g.p0[0], g.p0[1])
|
|
ctx.lineTo(g.p1[0], g.p1[1])
|
|
ctx.stroke()
|
|
break
|
|
}
|
|
default: break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export default ol_render3D
|