mapbox-gl
Version:
A WebGL interactive maps library
186 lines (151 loc) • 7.51 kB
JavaScript
// @flow
import styleSpec from '../style-spec/reference/latest.js';
import {extend, smoothstep} from '../util/util.js';
import {Evented} from '../util/evented.js';
import {validateStyle, validateFog, emitValidationErrors} from './validate_style.js';
import {Properties, Transitionable, Transitioning, PossiblyEvaluated, DataConstantProperty} from './properties.js';
import Color from '../style-spec/util/color.js';
import {FOG_PITCH_START, FOG_PITCH_END, FOG_OPACITY_THRESHOLD, getFogOpacityAtLngLat, getFogOpacityAtMercCoord, getFovAdjustedFogRange, getFogOpacityForBounds} from './fog_helpers.js';
import {number as interpolate, array as vecInterpolate} from '../style-spec/util/interpolate.js';
import {globeToMercatorTransition} from '../geo/projection/globe_util.js';
import {Frustum} from '../util/primitives.js';
import {OverscaledTileID} from '../source/tile_id.js';
import EXTENT from '../style-spec/data/extent.js';
import type {FogSpecification} from '../style-spec/types.js';
import type EvaluationParameters from './evaluation_parameters.js';
import type {TransitionParameters} from './properties.js';
import type LngLat from '../geo/lng_lat.js';
import type Transform from '../geo/transform.js';
import type {StyleSetterOptions} from '../style/style.js';
import type {FogState} from './fog_helpers.js';
import type {Mat4, Vec3} from 'gl-matrix';
type Props = {|
"range": DataConstantProperty<[number, number]>,
"color": DataConstantProperty<Color>,
"high-color": DataConstantProperty<Color>,
"space-color": DataConstantProperty<Color>,
"horizon-blend": DataConstantProperty<number>,
"star-intensity": DataConstantProperty<number>,
"vertical-range": DataConstantProperty<[number, number]>,
|};
const fogProperties: Properties<Props> = new Properties({
"range": new DataConstantProperty(styleSpec.fog.range),
"color": new DataConstantProperty(styleSpec.fog.color),
"high-color": new DataConstantProperty(styleSpec.fog["high-color"]),
"space-color": new DataConstantProperty(styleSpec.fog["space-color"]),
"horizon-blend": new DataConstantProperty(styleSpec.fog["horizon-blend"]),
"star-intensity": new DataConstantProperty(styleSpec.fog["star-intensity"]),
"vertical-range": new DataConstantProperty(styleSpec.fog["vertical-range"]),
});
class Fog extends Evented {
_transitionable: Transitionable<Props>;
_transitioning: Transitioning<Props>;
properties: PossiblyEvaluated<Props>;
// Alternate projections do not yet support fog.
// Hold on to transform so that we know whether a projection is set.
_transform: Transform;
constructor(fogOptions?: FogSpecification, transform: Transform) {
super();
this._transitionable = new Transitionable(fogProperties);
this.set(fogOptions);
this._transitioning = this._transitionable.untransitioned();
this._transform = transform;
}
get state(): FogState {
const tr = this._transform;
const isGlobe = tr.projection.name === 'globe';
const transitionT = globeToMercatorTransition(tr.zoom);
const range = this.properties.get('range');
const globeFixedFogRange = [0.5, 3];
return {
range: isGlobe ? [
interpolate(globeFixedFogRange[0], range[0], transitionT),
interpolate(globeFixedFogRange[1], range[1], transitionT)
] : range,
horizonBlend: this.properties.get('horizon-blend'),
alpha: this.properties.get('color').a
};
}
get(): FogSpecification {
return (this._transitionable.serialize(): any);
}
set(fog?: FogSpecification, options: StyleSetterOptions = {}) {
if (this._validate(validateFog, fog, options)) {
return;
}
const properties = extend({}, fog);
for (const name of Object.keys(styleSpec.fog)) {
// Fallback to use default style specification when the properties wasn't set
if (properties[name] === undefined) {
properties[name] = styleSpec.fog[name].default;
}
}
this._transitionable.setTransitionOrValue<FogSpecification>(properties);
}
getOpacity(pitch: number): number {
if (!this._transform.projection.supportsFog) return 0;
const fogColor = (this.properties && this.properties.get('color')) || 1.0;
const isGlobe = this._transform.projection.name === 'globe';
const pitchFactor = isGlobe ? 1.0 : smoothstep(FOG_PITCH_START, FOG_PITCH_END, pitch);
return pitchFactor * fogColor.a;
}
getOpacityAtLatLng(lngLat: LngLat, transform: Transform): number {
if (!this._transform.projection.supportsFog) return 0;
return getFogOpacityAtLngLat(this.state, lngLat, transform);
}
getOpacityForTile(id: OverscaledTileID): [number, number] {
if (!this._transform.projection.supportsFog) return [1, 1];
const fogMatrix = this._transform.calculateFogTileMatrix(id.toUnwrapped());
return getFogOpacityForBounds(this.state, fogMatrix, 0, 0, EXTENT, EXTENT, this._transform);
}
getOpacityForBounds(matrix: Mat4, x0: number, y0: number, x1: number, y1: number): [number, number] {
if (!this._transform.projection.supportsFog) return [1, 1];
return getFogOpacityForBounds(this.state, matrix, x0, y0, x1, y1, this._transform);
}
getFovAdjustedRange(fov: number): [number, number] {
// We can return any arbitrary range because we expect opacity=0 to clean it up
if (!this._transform.projection.supportsFog) return [0, 1];
return getFovAdjustedFogRange(this.state, fov);
}
isVisibleOnFrustum(frustum: Frustum): boolean {
if (!this._transform.projection.supportsFog) return false;
// Compute locations where frustum edges intersects with the ground plane
// and determine if all of these points are closer to the camera than
// the starting point (near range) of the fog.
const farPoints = [4, 5, 6, 7];
for (const pointIdx of farPoints) {
const farPoint = frustum.points[pointIdx];
let flatPoint: ?Vec3;
if (farPoint[2] >= 0.0) {
flatPoint = farPoint;
} else {
const nearPoint = frustum.points[pointIdx - 4];
flatPoint = vecInterpolate((nearPoint: any), (farPoint: any), nearPoint[2] / (nearPoint[2] - farPoint[2]));
}
if (getFogOpacityAtMercCoord(this.state, flatPoint[0], flatPoint[1], 0, this._transform) >= FOG_OPACITY_THRESHOLD) {
return true;
}
}
return false;
}
updateTransitions(parameters: TransitionParameters) {
this._transitioning = this._transitionable.transitioned(parameters, this._transitioning);
}
hasTransition(): boolean {
return this._transitioning.hasTransition();
}
recalculate(parameters: EvaluationParameters) {
this.properties = this._transitioning.possiblyEvaluate(parameters);
}
_validate(validate: Function, value: mixed, options?: {validate?: boolean}): boolean {
if (options && options.validate === false) {
return false;
}
return emitValidationErrors(this, validate.call(validateStyle, extend({
value,
style: {glyphs: true, sprite: true},
styleSpec
})));
}
}
export default Fog;