Merge remote-tracking branch 'origin/master' into fix_triangulation

This commit is contained in:
philip
2020-04-02 22:58:59 +00:00
217 changed files with 6848 additions and 3791 deletions

View File

@@ -26,7 +26,7 @@ export class CollectionEvent extends Event {
/**
* @param {CollectionEventType} type Type.
* @param {*=} opt_element Element.
* @param {number} opt_index The index of the added or removed element.
* @param {number=} opt_index The index of the added or removed element.
*/
constructor(type, opt_element, opt_index) {
super(type);

View File

@@ -275,7 +275,9 @@ class MapBrowserEventHandler extends EventTarget {
* @private
*/
handleTouchMove_(event) {
if (this.originalPointerMoveEvent_.defaultPrevented) {
// Due to https://github.com/mpizenberg/elm-pep/issues/2, `this.originalPointerMoveEvent_`
// may not be initialized yet when we get here on a platform without native pointer events.
if (!this.originalPointerMoveEvent_ || this.originalPointerMoveEvent_.defaultPrevented) {
event.preventDefault();
}
}

View File

@@ -51,7 +51,7 @@ class Observable extends EventTarget {
/**
* Listen for a certain type of event.
* @param {string|Array<string>} type The event type or array of event types.
* @param {function(?): ?} listener The listener function.
* @param {import("./events.js").ListenerFunction} listener The listener function.
* @return {import("./events.js").EventsKey|Array<import("./events.js").EventsKey>} Unique key for the listener. If
* called with an array of event types as the first argument, the return
* will be an array of keys.

View File

@@ -37,15 +37,21 @@ import {containsExtent} from './extent.js';
* container as that of the controls (see the `stopEvent` option) you will
* probably set `insertFirst` to `true` so the overlay is displayed below the
* controls.
* @property {boolean} [autoPan=false] If set to `true` the map is panned when
* calling `setPosition`, so that the overlay is entirely visible in the current
* viewport.
* @property {PanOptions} [autoPanAnimation] The
* animation options used to pan the overlay into view. This animation is only
* used when `autoPan` is enabled. A `duration` and `easing` may be provided to
* customize the animation.
* @property {PanIntoViewOptions|boolean} [autoPan=false] Pan the map when calling
* `setPosition`, so that the overlay is entirely visible in the current viewport?
* If `true` (deprecated), then `autoPanAnimation` and `autoPanMargin` will be
* used to determine the panning parameters; if an object is supplied then other
* parameters are ignored.
* @property {PanOptions} [autoPanAnimation] The animation options used to pan
* the overlay into view. This animation is only used when `autoPan` is enabled.
* A `duration` and `easing` may be provided to customize the animation.
* Deprecated and ignored if `autoPan` is supplied as an object.
* @property {number} [autoPanMargin=20] The margin (in pixels) between the
* overlay and the borders of the map when autopanning.
* overlay and the borders of the map when autopanning. Deprecated and ignored
* if `autoPan` is supplied as an object.
* @property {PanIntoViewOptions} [autoPanOptions] The options to use for the
* autoPan. This is only used when `autoPan` is enabled and has preference over
* the individual `autoPanMargin` and `autoPanOptions`.
* @property {string} [className='ol-overlay-container ol-selectable'] CSS class
* name.
*/
@@ -60,6 +66,12 @@ import {containsExtent} from './extent.js';
* Default is {@link module:ol/easing~inAndOut}.
*/
/**
* @typedef {Object} PanIntoViewOptions
* @property {PanOptions} [animation={}] The animation parameters for the pan
* @property {number} [margin=20] The margin (in pixels) between the
* overlay and the borders of the map when panning into view.
*/
/**
* @enum {string}
@@ -137,38 +149,26 @@ class Overlay extends BaseObject {
options.className : 'ol-overlay-container ' + CLASS_SELECTABLE;
this.element.style.position = 'absolute';
let autoPan = options.autoPan;
if (autoPan && ('object' !== typeof autoPan)) {
autoPan = {
animation: options.autoPanAnimation,
margin: options.autoPanMargin
};
}
/**
* @protected
* @type {boolean}
* @type {PanIntoViewOptions|false}
*/
this.autoPan = options.autoPan !== undefined ? options.autoPan : false;
this.autoPan = /** @type {PanIntoViewOptions} */(autoPan) || false;
/**
* @protected
* @type {PanOptions}
*/
this.autoPanAnimation = options.autoPanAnimation || /** @type {PanOptions} */ ({});
/**
* @protected
* @type {number}
*/
this.autoPanMargin = options.autoPanMargin !== undefined ?
options.autoPanMargin : 20;
/**
* @protected
* @type {{bottom_: string,
* left_: string,
* right_: string,
* top_: string,
* @type {{transform_: string,
* visible: boolean}}
*/
this.rendered = {
bottom_: '',
left_: '',
right_: '',
top_: '',
transform_: '',
visible: true
};
@@ -300,6 +300,7 @@ class Overlay extends BaseObject {
} else {
container.appendChild(this.element);
}
this.performAutoPan();
}
}
@@ -322,9 +323,7 @@ class Overlay extends BaseObject {
*/
handlePositionChanged() {
this.updatePixelPosition();
if (this.get(Property.POSITION) && this.autoPan) {
this.panIntoView();
}
this.performAutoPan();
}
/**
@@ -378,14 +377,26 @@ class Overlay extends BaseObject {
}
/**
* Pan the map so that the overlay is entirely visible in the current viewport
* (if necessary).
* Pan the map so that the overlay is entirely visisble in the current viewport
* (if necessary) using the configured autoPan parameters
* @protected
*/
panIntoView() {
performAutoPan() {
if (this.autoPan) {
this.panIntoView(this.autoPan);
}
}
/**
* Pan the map so that the overlay is entirely visible in the current viewport
* (if necessary).
* @param {PanIntoViewOptions|undefined} panIntoViewOptions Options for the pan action
* @api
*/
panIntoView(panIntoViewOptions) {
const map = this.getMap();
if (!map || !map.getTargetElement()) {
if (!map || !map.getTargetElement() || !this.get(Property.POSITION)) {
return;
}
@@ -393,7 +404,7 @@ class Overlay extends BaseObject {
const element = this.getElement();
const overlayRect = this.getRect(element, [outerWidth(element), outerHeight(element)]);
const margin = this.autoPanMargin;
const myMargin = (panIntoViewOptions.margin === undefined) ? 20 : panIntoViewOptions.margin;
if (!containsExtent(mapRect, overlayRect)) {
// the overlay is not completely inside the viewport, so pan the map
const offsetLeft = overlayRect[0] - mapRect[0];
@@ -404,17 +415,17 @@ class Overlay extends BaseObject {
const delta = [0, 0];
if (offsetLeft < 0) {
// move map to the left
delta[0] = offsetLeft - margin;
delta[0] = offsetLeft - myMargin;
} else if (offsetRight < 0) {
// move map to the right
delta[0] = Math.abs(offsetRight) + margin;
delta[0] = Math.abs(offsetRight) + myMargin;
}
if (offsetTop < 0) {
// move map up
delta[1] = offsetTop - margin;
delta[1] = offsetTop - myMargin;
} else if (offsetBottom < 0) {
// move map down
delta[1] = Math.abs(offsetBottom) + margin;
delta[1] = Math.abs(offsetBottom) + myMargin;
}
if (delta[0] !== 0 || delta[1] !== 0) {
@@ -425,10 +436,11 @@ class Overlay extends BaseObject {
centerPx[1] + delta[1]
];
const panOptions = panIntoViewOptions.animation || {};
map.getView().animateInternal({
center: map.getCoordinateFromPixelInternal(newCenterPx),
duration: this.autoPanAnimation.duration,
easing: this.autoPanAnimation.easing
duration: panOptions.duration,
easing: panOptions.easing
});
}
}
@@ -506,63 +518,34 @@ class Overlay extends BaseObject {
this.setVisible(true);
let offsetX = offset[0];
let offsetY = offset[1];
const x = Math.round(pixel[0] + offset[0]) + 'px';
const y = Math.round(pixel[1] + offset[1]) + 'px';
let posX = '0%';
let posY = '0%';
if (positioning == OverlayPositioning.BOTTOM_RIGHT ||
positioning == OverlayPositioning.CENTER_RIGHT ||
positioning == OverlayPositioning.TOP_RIGHT) {
if (this.rendered.left_ !== '') {
this.rendered.left_ = '';
style.left = '';
}
const right = Math.round(mapSize[0] - pixel[0] - offsetX) + 'px';
if (this.rendered.right_ != right) {
this.rendered.right_ = right;
style.right = right;
}
} else {
if (this.rendered.right_ !== '') {
this.rendered.right_ = '';
style.right = '';
}
if (positioning == OverlayPositioning.BOTTOM_CENTER ||
positioning == OverlayPositioning.CENTER_CENTER ||
positioning == OverlayPositioning.TOP_CENTER) {
offsetX -= this.element.offsetWidth / 2;
}
const left = Math.round(pixel[0] + offsetX) + 'px';
if (this.rendered.left_ != left) {
this.rendered.left_ = left;
style.left = left;
}
posX = '-100%';
} else if (positioning == OverlayPositioning.BOTTOM_CENTER ||
positioning == OverlayPositioning.CENTER_CENTER ||
positioning == OverlayPositioning.TOP_CENTER) {
posX = '-50%';
}
if (positioning == OverlayPositioning.BOTTOM_LEFT ||
positioning == OverlayPositioning.BOTTOM_CENTER ||
positioning == OverlayPositioning.BOTTOM_RIGHT) {
if (this.rendered.top_ !== '') {
this.rendered.top_ = '';
style.top = '';
}
const bottom = Math.round(mapSize[1] - pixel[1] - offsetY) + 'px';
if (this.rendered.bottom_ != bottom) {
this.rendered.bottom_ = bottom;
style.bottom = bottom;
}
} else {
if (this.rendered.bottom_ !== '') {
this.rendered.bottom_ = '';
style.bottom = '';
}
if (positioning == OverlayPositioning.CENTER_LEFT ||
positioning == OverlayPositioning.CENTER_CENTER ||
positioning == OverlayPositioning.CENTER_RIGHT) {
offsetY -= this.element.offsetHeight / 2;
}
const top = Math.round(pixel[1] + offsetY) + 'px';
if (this.rendered.top_ != top) {
this.rendered.top_ = 'top';
style.top = top;
}
posY = '-100%';
} else if (positioning == OverlayPositioning.CENTER_LEFT ||
positioning == OverlayPositioning.CENTER_CENTER ||
positioning == OverlayPositioning.CENTER_RIGHT) {
posY = '-50%';
}
const transform = `translate(${posX}, ${posY}) translate(${x}, ${y})`;
if (this.rendered.transform_ != transform) {
this.rendered.transform_ = transform;
style.transform = transform;
// @ts-ignore IE9
style.msTransform = transform;
}
}

View File

@@ -12,7 +12,7 @@ import MapProperty from './MapProperty.js';
import RenderEventType from './render/EventType.js';
import BaseObject, {getChangeEventType} from './Object.js';
import ObjectEventType from './ObjectEventType.js';
import TileQueue from './TileQueue.js';
import TileQueue, {getTilePriority} from './TileQueue.js';
import View from './View.js';
import ViewHint from './ViewHint.js';
import {assert} from './asserts.js';
@@ -24,7 +24,6 @@ import {TRUE} from './functions.js';
import {DEVICE_PIXEL_RATIO, IMAGE_DECODE, PASSIVE_EVENT_LISTENERS} from './has.js';
import LayerGroup from './layer/Group.js';
import {hasArea} from './size.js';
import {DROP} from './structs/PriorityQueue.js';
import {create as createTransform, apply as applyTransform} from './transform.js';
import {toUserCoordinate, fromUserCoordinate} from './proj.js';
@@ -891,26 +890,7 @@ class PluggableMap extends BaseObject {
* @return {number} Tile priority.
*/
getTilePriority(tile, tileSourceKey, tileCenter, tileResolution) {
// Filter out tiles at higher zoom levels than the current zoom level, or that
// are outside the visible extent.
const frameState = this.frameState_;
if (!frameState || !(tileSourceKey in frameState.wantedTiles)) {
return DROP;
}
if (!frameState.wantedTiles[tileSourceKey][tile.getKey()]) {
return DROP;
}
// Prioritize the highest zoom level tiles closest to the focus.
// Tiles at higher zoom levels are prioritized using Math.log(tileResolution).
// Within a zoom level, tiles are prioritized by the distance in pixels between
// the center of the tile and the center of the viewport. The factor of 65536
// means that the prioritization should behave as desired for tiles up to
// 65536 * Math.log(2) = 45426 pixels from the focus.
const center = frameState.viewState.center;
const deltaX = tileCenter[0] - center[0];
const deltaY = tileCenter[1] - center[1];
return 65536 * Math.log(tileResolution) +
Math.sqrt(deltaX * deltaX + deltaY * deltaY) / tileResolution;
return getTilePriority(this.frameState_, tile, tileSourceKey, tileCenter, tileResolution);
}
/**
@@ -932,13 +912,15 @@ class PluggableMap extends BaseObject {
// coordinates so interactions cannot be used.
return;
}
let target = /** @type {Node} */ (mapBrowserEvent.originalEvent.target);
const target = /** @type {Node} */ (mapBrowserEvent.originalEvent.target);
if (!mapBrowserEvent.dragging) {
while (target && target !== this.viewport_) {
if (target.parentElement === this.overlayContainerStopEvent_) {
return;
}
target = target.parentElement;
if (this.overlayContainerStopEvent_.contains(target) || !(document.body.contains(target) || this.viewport_.getRootNode && this.viewport_.getRootNode().contains(target))) {
// Abort if the event target is a child of the container that doesn't allow
// event propagation or is no longer in the page. It's possible for the target to no longer
// be in the page if it has been removed in an event listener, this might happen in a Control
// that recreates it's content based on user interaction either manually or via a render
// in something like https://reactjs.org/
return;
}
}
mapBrowserEvent.frameState = this.frameState_;

View File

@@ -7,7 +7,7 @@ import {fromKey, getKey} from './tilecoord.js';
class TileCache extends LRUCache {
/**
* @param {!Object<string, import("./TileRange.js").default>} usedTiles Used tiles.
* @param {!Object<string, boolean>} usedTiles Used tiles.
*/
expireCache(usedTiles) {
while (this.canExpireCache()) {

View File

@@ -3,7 +3,7 @@
*/
import TileState from './TileState.js';
import EventType from './events/EventType.js';
import PriorityQueue from './structs/PriorityQueue.js';
import PriorityQueue, {DROP} from './structs/PriorityQueue.js';
/**
@@ -119,3 +119,34 @@ class TileQueue extends PriorityQueue {
export default TileQueue;
/**
* @param {import('./PluggableMap.js').FrameState} frameState Frame state.
* @param {import("./Tile.js").default} tile Tile.
* @param {string} tileSourceKey Tile source key.
* @param {import("./coordinate.js").Coordinate} tileCenter Tile center.
* @param {number} tileResolution Tile resolution.
* @return {number} Tile priority.
*/
export function getTilePriority(frameState, tile, tileSourceKey, tileCenter, tileResolution) {
// Filter out tiles at higher zoom levels than the current zoom level, or that
// are outside the visible extent.
if (!frameState || !(tileSourceKey in frameState.wantedTiles)) {
return DROP;
}
if (!frameState.wantedTiles[tileSourceKey][tile.getKey()]) {
return DROP;
}
// Prioritize the highest zoom level tiles closest to the focus.
// Tiles at higher zoom levels are prioritized using Math.log(tileResolution).
// Within a zoom level, tiles are prioritized by the distance in pixels between
// the center of the tile and the center of the viewport. The factor of 65536
// means that the prioritization should behave as desired for tiles up to
// 65536 * Math.log(2) = 45426 pixels from the focus.
const center = frameState.viewState.center;
const deltaX = tileCenter[0] - center[0];
const deltaY = tileCenter[1] - center[1];
return 65536 * Math.log(tileResolution) +
Math.sqrt(deltaX * deltaX + deltaY * deltaY) / tileResolution;
}

View File

@@ -129,6 +129,14 @@ import {createMinMaxResolution} from './resolutionconstraint.js';
* @property {boolean} [smoothResolutionConstraint=true] If true, the resolution
* min/max values will be applied smoothly, i. e. allow the view to exceed slightly
* the given resolution or zoom bounds.
* @property {boolean} [showFullExtent=false] Allow the view to be zoomed out to
* show the full configured extent. By default, when a view is configured with an
* extent, users will not be able to zoom out so the viewport exceeds the extent in
* either dimension. This means the full extent may not be visible if the viewport
* is taller or wider than the aspect ratio of the configured extent. If
* showFullExtent is true, the user will be able to zoom out so that the viewport
* exceeds the height or width of the configured extent, but not both, allowing the
* full extent to be shown.
* @property {import("./proj.js").ProjectionLike} [projection='EPSG:3857'] The
* projection. The default is Spherical Mercator.
* @property {number} [resolution] The initial resolution for the view. The
@@ -397,7 +405,6 @@ class View extends BaseObject {
} else if (options.zoom !== undefined) {
this.setZoom(options.zoom);
}
this.resolveConstraints(0);
this.setProperties(properties);
@@ -601,10 +608,15 @@ class View extends BaseObject {
if (series[0].callback) {
animationCallback(series[0].callback, false);
}
anchor = anchor ||
series.filter(function(animation) {
return !animation.complete;
})[0].anchor;
if (!anchor) {
for (let j = 0, jj = series.length; j < jj; ++j) {
const animation = series[j];
if (!animation.complete) {
anchor = animation.anchor;
break;
}
}
}
}
this.animations_.length = 0;
this.cancelAnchor_ = anchor;
@@ -754,6 +766,7 @@ class View extends BaseObject {
*/
setViewportSize(opt_size) {
this.viewportSize_ = Array.isArray(opt_size) ? opt_size.slice() : [100, 100];
this.resolveConstraints(0);
}
/**
@@ -785,6 +798,13 @@ class View extends BaseObject {
return this.constraints_;
}
/**
* @return {boolean} Resolution constraint is set
*/
getConstrainResolution() {
return this.options_.constrainResolution;
}
/**
* @param {Array<number>=} opt_hints Destination array.
* @return {Array<number>} Hint.
@@ -1611,6 +1631,9 @@ export function createResolutionConstraint(options) {
const smooth =
options.smoothResolutionConstraint !== undefined ? options.smoothResolutionConstraint : true;
const showFullExtent =
options.showFullExtent !== undefined ? options.showFullExtent : false;
const projection = createProjection(options.projection, 'EPSG:3857');
const projExtent = projection.getExtent();
let constrainOnlyCenter = options.constrainOnlyCenter;
@@ -1628,10 +1651,10 @@ export function createResolutionConstraint(options) {
if (options.constrainResolution) {
resolutionConstraint = createSnapToResolutions(resolutions, smooth,
!constrainOnlyCenter && extent);
!constrainOnlyCenter && extent, showFullExtent);
} else {
resolutionConstraint = createMinMaxResolution(maxResolution, minResolution, smooth,
!constrainOnlyCenter && extent);
!constrainOnlyCenter && extent, showFullExtent);
}
} else {
// calculate the default min and max resolution
@@ -1677,10 +1700,10 @@ export function createResolutionConstraint(options) {
if (options.constrainResolution) {
resolutionConstraint = createSnapToPower(
zoomFactor, maxResolution, minResolution, smooth,
!constrainOnlyCenter && extent);
!constrainOnlyCenter && extent, showFullExtent);
} else {
resolutionConstraint = createMinMaxResolution(maxResolution, minResolution, smooth,
!constrainOnlyCenter && extent);
!constrainOnlyCenter && extent, showFullExtent);
}
}
return {constraint: resolutionConstraint, maxResolution: maxResolution,

View File

@@ -325,7 +325,6 @@ class Attribution extends Control {
* Update the attribution element.
* @param {import("../MapEvent.js").default} mapEvent Map event.
* @this {Attribution}
* @api
*/
export function render(mapEvent) {
this.updateElement_(mapEvent.frameState);

View File

@@ -79,9 +79,10 @@ class Control extends BaseObject {
this.listenerKeys = [];
/**
* @private
* @type {function(import("../MapEvent.js").default): void}
*/
this.render = options.render ? options.render : VOID;
this.render_ = options.render ? options.render : VOID;
if (options.target) {
this.setTarget(options.target);
@@ -134,6 +135,16 @@ class Control extends BaseObject {
}
}
/**
* Update the projection. Rendering of the coordinates is done in
* `handleMouseMove` and `handleMouseUp`.
* @param {import("../MapEvent.js").default} mapEvent Map event.
* @api
*/
render(mapEvent) {
this.render_.call(this, mapEvent);
}
/**
* This function is used to set a target element for the control. It has no
* effect if it is called after the control has been added to the map (i.e.

View File

@@ -9,6 +9,29 @@ import EventType from '../events/EventType.js';
const events = ['fullscreenchange', 'webkitfullscreenchange', 'MSFullscreenChange'];
/**
* @enum {string}
*/
const FullScreenEventType = {
/**
* Triggered after the map entered fullscreen.
* @event FullScreenEventType#enterfullscreen
* @api
*/
ENTERFULLSCREEN: 'enterfullscreen',
/**
* Triggered after the map leave fullscreen.
* @event FullScreenEventType#leavefullscreen
* @api
*/
LEAVEFULLSCREEN: 'leavefullscreen'
};
/**
* @typedef {Object} Options
* @property {string} [className='ol-full-screen'] CSS class name.
@@ -38,6 +61,8 @@ const events = ['fullscreenchange', 'webkitfullscreenchange', 'MSFullscreenChang
* The [Fullscreen API](http://www.w3.org/TR/fullscreen/) is used to
* toggle the map in full screen mode.
*
* @fires FullScreenEventType#enterfullscreen
* @fires FullScreenEventType#leavefullscreen
* @api
*/
class FullScreen extends Control {
@@ -162,9 +187,11 @@ class FullScreen extends Control {
if (isFullScreen()) {
this.setClassName_(this.button_, true);
replaceNode(this.labelActiveNode_, this.labelNode_);
this.dispatchEvent(FullScreenEventType.ENTERFULLSCREEN);
} else {
this.setClassName_(this.button_, false);
replaceNode(this.labelNode_, this.labelActiveNode_);
this.dispatchEvent(FullScreenEventType.LEAVEFULLSCREEN);
}
if (map) {
map.updateSize();

View File

@@ -248,7 +248,6 @@ class MousePosition extends Control {
* `handleMouseMove` and `handleMouseUp`.
* @param {import("../MapEvent.js").default} mapEvent Map event.
* @this {MousePosition}
* @api
*/
export function render(mapEvent) {
const frameState = mapEvent.frameState;

View File

@@ -17,6 +17,7 @@ import {replaceNode} from '../dom.js';
import {listen, listenOnce} from '../events.js';
import EventType from '../events/EventType.js';
import {containsExtent, equals as equalsExtent, getBottomRight, getTopLeft, scaleFromCenter} from '../extent.js';
import View from '../View.js';
/**
@@ -59,8 +60,8 @@ class ControlledMap extends PluggableMap {
* @property {HTMLElement|string} [target] Specify a target if you want the control
* to be rendered outside of the map's viewport.
* @property {string} [tipLabel='Overview map'] Text label to use for the button tip.
* @property {import("../View.js").default} [view] Custom view for the overview map. If not provided,
* a default view with an EPSG:3857 projection will be used.
* @property {View} [view] Custom view for the overview map (should use same projection as main map). If not provided,
* a default view with the same projection as the main map will be used.
*/
@@ -167,6 +168,13 @@ class OverviewMap extends Control {
this.ovmapDiv_ = document.createElement('div');
this.ovmapDiv_.className = 'ol-overviewmap-map';
/**
* Explicitly given view to be used instead of a view derived from the main map.
* @type {View}
* @private
*/
this.view_ = options.view;
/**
* @type {ControlledMap}
* @private
@@ -303,6 +311,14 @@ class OverviewMap extends Control {
* @private
*/
bindView_(view) {
if (!this.view_) {
// Unless an explicit view definition was given, derive default from whatever main map uses.
const newView = new View({
projection: view.getProjection()
});
this.ovmap_.setView(newView);
}
view.addEventListener(getChangeEventType(ViewProperty.ROTATION), this.boundHandleRotationChanged_);
// Sync once with the new view
this.handleRotationChanged_();
@@ -603,7 +619,6 @@ class OverviewMap extends Control {
* Update the overview map element.
* @param {import("../MapEvent.js").default} mapEvent Map event.
* @this {OverviewMap}
* @api
*/
export function render(mapEvent) {
this.validateExtent_();

View File

@@ -131,8 +131,9 @@ class Rotate extends Control {
// upon it
return;
}
if (view.getRotation() !== undefined) {
if (this.duration_ > 0) {
const rotation = view.getRotation();
if (rotation !== undefined) {
if (this.duration_ > 0 && rotation % (2 * Math.PI) !== 0) {
view.animate({
rotation: 0,
duration: this.duration_,
@@ -150,7 +151,6 @@ class Rotate extends Control {
* Update the rotate control element.
* @param {import("../MapEvent.js").default} mapEvent Map event.
* @this {Rotate}
* @api
*/
export function render(mapEvent) {
const frameState = mapEvent.frameState;

View File

@@ -418,7 +418,6 @@ class ScaleLine extends Control {
* Update the scale line element.
* @param {import("../MapEvent.js").default} mapEvent Map event.
* @this {ScaleLine}
* @api
*/
export function render(mapEvent) {
const frameState = mapEvent.frameState;

View File

@@ -339,7 +339,6 @@ class ZoomSlider extends Control {
* Update the zoomslider element.
* @param {import("../MapEvent.js").default} mapEvent Map event.
* @this {ZoomSlider}
* @api
*/
export function render(mapEvent) {
if (!mapEvent.frameState) {

View File

@@ -3,6 +3,7 @@
*/
import {modulo} from './math.js';
import {padNumber} from './string.js';
import {getWidth} from './extent.js';
/**
@@ -402,3 +403,23 @@ export function toStringHDMS(coordinate, opt_fractionDigits) {
export function toStringXY(coordinate, opt_fractionDigits) {
return format(coordinate, '{x}, {y}', opt_fractionDigits);
}
/**
* Modifies the provided coordinate in-place to be within the real world
* extent. The lower projection extent boundary is inclusive, the upper one
* exclusive.
*
* @param {Coordinate} coordinate Coordinate.
* @param {import("./proj/Projection.js").default} projection Projection
* @return {Coordinate} The coordinate within the real world extent.
*/
export function wrapX(coordinate, projection) {
const projectionExtent = projection.getExtent();
if (projection.canWrapX() && (coordinate[0] < projectionExtent[0] || coordinate[0] >= projectionExtent[2])) {
const worldWidth = getWidth(projectionExtent);
const worldsAway = Math.floor((coordinate[0] - projectionExtent[0]) / worldWidth);
coordinate[0] -= (worldsAway * worldWidth);
}
return coordinate;
}

View File

@@ -4,9 +4,13 @@
/**
* @typedef {Object} FontParameters
* @property {Array<string>} families
* @property {string} style
* @property {string} variant
* @property {string} weight
* @property {string} size
* @property {string} lineHeight
* @property {string} family
* @property {Array<string>} families
*/
@@ -64,42 +68,52 @@ export const CLASS_CONTROL = 'ol-control';
*/
export const CLASS_COLLAPSED = 'ol-collapsed';
/**
* From http://stackoverflow.com/questions/10135697/regex-to-parse-any-css-font
* @type {RegExp}
*/
const fontRegEx = new RegExp([
'^\\s*(?=(?:(?:[-a-z]+\\s*){0,2}(italic|oblique))?)',
'(?=(?:(?:[-a-z]+\\s*){0,2}(small-caps))?)',
'(?=(?:(?:[-a-z]+\\s*){0,2}(bold(?:er)?|lighter|[1-9]00 ))?)',
'(?:(?:normal|\\1|\\2|\\3)\\s*){0,3}((?:xx?-)?',
'(?:small|large)|medium|smaller|larger|[\\.\\d]+(?:\\%|in|[cem]m|ex|p[ctx]))',
'(?:\\s*\\/\\s*(normal|[\\.\\d]+(?:\\%|in|[cem]m|ex|p[ctx])?))',
'?\\s*([-,\\"\\\'\\sa-z]+?)\\s*$'
].join(''), 'i');
const fontRegExMatchIndex = [
'style',
'variant',
'weight',
'size',
'lineHeight',
'family'
];
/**
* Get the list of font families from a font spec. Note that this doesn't work
* for font families that have commas in them.
* @param {string} The CSS font property.
* @return {FontParameters} The font families (or null if the input spec is invalid).
* @param {string} fontSpec The CSS font property.
* @return {FontParameters} The font parameters (or null if the input spec is invalid).
*/
export const getFontParameters = (function() {
/**
* @type {CSSStyleDeclaration}
*/
let style;
/**
* @type {Object<string, FontParameters>}
*/
const cache = {};
return function(font) {
if (!style) {
style = document.createElement('div').style;
export const getFontParameters = function(fontSpec) {
const match = fontSpec.match(fontRegEx);
if (!match) {
return null;
}
const style = /** @type {FontParameters} */ ({
lineHeight: 'normal',
size: '1.2em',
style: 'normal',
weight: 'normal',
variant: 'normal'
});
for (let i = 0, ii = fontRegExMatchIndex.length; i < ii; ++i) {
const value = match[i + 1];
if (value !== undefined) {
style[fontRegExMatchIndex[i]] = value;
}
if (!(font in cache)) {
style.font = font;
const family = style.fontFamily;
const fontWeight = style.fontWeight;
const fontStyle = style.fontStyle;
style.font = '';
if (!family) {
return null;
}
const families = family.split(/,\s?/);
cache[font] = {
families: families,
weight: fontWeight,
style: fontStyle
};
}
return cache[font];
};
})();
}
style.families = style.family.split(/,\s?/);
return style;
};

View File

@@ -1,8 +1,11 @@
import {WORKER_OFFSCREEN_CANVAS} from './has.js';
/**
* @module ol/dom
*/
//FIXME Move this function to the canvas module
/**
* Create an html canvas element and returns its 2d context.
* @param {number=} opt_width Canvas width.
@@ -12,14 +15,18 @@
*/
export function createCanvasContext2D(opt_width, opt_height, opt_canvasPool) {
const canvas = opt_canvasPool && opt_canvasPool.length ?
opt_canvasPool.shift() : document.createElement('canvas');
opt_canvasPool.shift() :
WORKER_OFFSCREEN_CANVAS ?
new OffscreenCanvas(opt_width || 300, opt_height || 300) :
document.createElement('canvas');
if (opt_width) {
canvas.width = opt_width;
}
if (opt_height) {
canvas.height = opt_height;
}
return canvas.getContext('2d');
//FIXME Allow OffscreenCanvasRenderingContext2D as return type
return /** @type {CanvasRenderingContext2D} */ (canvas.getContext('2d'));
}

View File

@@ -13,7 +13,6 @@ import {clear} from './obj.js';
* @api
*/
/**
* Listener function. This function is called with an event object as argument.
* When the function returns `false`, event propagation will stop.
@@ -22,6 +21,14 @@ import {clear} from './obj.js';
* @api
*/
/**
* @typedef {Object} ListenerObject
* @property {ListenerFunction} handleEvent
*/
/**
* @typedef {ListenerFunction|ListenerObject} Listener
*/
/**
* Registers an event listener on an event target. Inspired by

View File

@@ -56,7 +56,7 @@ class Target extends Disposable {
/**
* @private
* @type {!Object<string, Array<import("../events.js").ListenerFunction>>}
* @type {!Object<string, Array<import("../events.js").Listener>>}
*/
this.listeners_ = {};
@@ -64,7 +64,7 @@ class Target extends Disposable {
/**
* @param {string} type Type.
* @param {import("../events.js").ListenerFunction} listener Listener.
* @param {import("../events.js").Listener} listener Listener.
*/
addEventListener(type, listener) {
if (!type || !listener) {
@@ -85,15 +85,13 @@ class Target extends Disposable {
* of this type. The event parameter can either be a string or an
* Object with a `type` property.
*
* @param {{type: string,
* target: (EventTargetLike|undefined),
* propagationStopped: (boolean|undefined)}|
* import("./Event.js").default|string} event Event object.
* @param {import("./Event.js").default|string} event Event object.
* @return {boolean|undefined} `false` if anyone called preventDefault on the
* event object or if any of the listeners returned false.
* @api
*/
dispatchEvent(event) {
/** @type {import("./Event.js").default|Event} */
const evt = typeof event === 'string' ? new Event(event) : event;
const type = evt.type;
if (!evt.target) {
@@ -108,7 +106,12 @@ class Target extends Disposable {
}
++this.dispatching_[type];
for (let i = 0, ii = listeners.length; i < ii; ++i) {
if (listeners[i].call(this, evt) === false || evt.propagationStopped) {
if ('handleEvent' in listeners[i]) {
propagate = /** @type {import("../events.js").ListenerObject} */ (listeners[i]).handleEvent(evt);
} else {
propagate = /** @type {import("../events.js").ListenerFunction} */ (listeners[i]).call(this, evt);
}
if (propagate === false || evt.propagationStopped) {
propagate = false;
break;
}
@@ -138,7 +141,7 @@ class Target extends Disposable {
* order that they will be called in.
*
* @param {string} type Type.
* @return {Array<import("../events.js").ListenerFunction>} Listeners.
* @return {Array<import("../events.js").Listener>} Listeners.
*/
getListeners(type) {
return this.listeners_[type];
@@ -157,7 +160,7 @@ class Target extends Disposable {
/**
* @param {string} type Type.
* @param {import("../events.js").ListenerFunction} listener Listener.
* @param {import("../events.js").Listener} listener Listener.
*/
removeEventListener(type, listener) {
const listeners = this.listeners_[type];

View File

@@ -295,6 +295,18 @@ export function equals(extent1, extent2) {
extent1[1] == extent2[1] && extent1[3] == extent2[3];
}
/**
* Determine if two extents are approximately equivalent.
* @param {Extent} extent1 Extent 1.
* @param {Extent} extent2 Extent 2.
* @param {number} tolerance Tolerance in extent coordinate units.
* @return {boolean} The two extents differ by less than the tolerance.
*/
export function approximatelyEquals(extent1, extent2, tolerance) {
return Math.abs(extent1[0] - extent2[0]) < tolerance && Math.abs(extent1[2] - extent2[2]) < tolerance &&
Math.abs(extent1[1] - extent2[1]) < tolerance && Math.abs(extent1[3] - extent2[3]) < tolerance;
}
/**
* Modify an extent to include another extent.
@@ -778,18 +790,60 @@ export function intersectsSegment(extent, start, end) {
* @param {import("./proj.js").TransformFunction} transformFn Transform function.
* Called with `[minX, minY, maxX, maxY]` extent coordinates.
* @param {Extent=} opt_extent Destination extent.
* @param {number=} opt_stops Number of stops per side used for the transform.
* By default only the corners are used.
* @return {Extent} Extent.
* @api
*/
export function applyTransform(extent, transformFn, opt_extent) {
const coordinates = [
extent[0], extent[1],
extent[0], extent[3],
extent[2], extent[1],
extent[2], extent[3]
];
export function applyTransform(extent, transformFn, opt_extent, opt_stops) {
let coordinates = [];
if (opt_stops > 1) {
const width = extent[2] - extent[0];
const height = extent[3] - extent[1];
for (let i = 0; i < opt_stops; ++i) {
coordinates.push(
extent[0] + width * i / opt_stops, extent[1],
extent[2], extent[1] + height * i / opt_stops,
extent[2] - width * i / opt_stops, extent[3],
extent[0], extent[3] - height * i / opt_stops
);
}
} else {
coordinates = [
extent[0], extent[1],
extent[2], extent[1],
extent[2], extent[3],
extent[0], extent[3]
];
}
transformFn(coordinates, coordinates, 2);
const xs = [coordinates[0], coordinates[2], coordinates[4], coordinates[6]];
const ys = [coordinates[1], coordinates[3], coordinates[5], coordinates[7]];
const xs = [];
const ys = [];
for (let i = 0, l = coordinates.length; i < l; i += 2) {
xs.push(coordinates[i]);
ys.push(coordinates[i + 1]);
}
return _boundingExtentXYs(xs, ys, opt_extent);
}
/**
* Modifies the provided extent in-place to be within the real world
* extent.
*
* @param {Extent} extent Extent.
* @param {import("./proj/Projection.js").default} projection Projection
* @return {Extent} The extent within the real world extent.
*/
export function wrapX(extent, projection) {
const projectionExtent = projection.getExtent();
const center = getCenter(extent);
if (projection.canWrapX() && (center[0] < projectionExtent[0] || center[0] >= projectionExtent[2])) {
const worldWidth = getWidth(projectionExtent);
const worldsAway = Math.floor((center[0] - projectionExtent[0]) / worldWidth);
const offset = (worldsAway * worldWidth);
extent[0] -= offset;
extent[2] -= offset;
}
return extent;
}

View File

@@ -162,6 +162,8 @@ class GeoJSON extends JSONFeature {
if (crs) {
if (crs['type'] == 'name') {
projection = getProjection(crs['properties']['name']);
} else if (crs['type'] === 'EPSG') {
projection = getProjection('EPSG:' + crs['properties']['code']);
} else {
assert(false, 36); // Unknown SRS type
}

View File

@@ -349,7 +349,7 @@ class IIIFInfo {
}
/**
* @param {PreferredOptions} opt_preferredOptions Optional options for preferred format and quality.
* @param {PreferredOptions=} opt_preferredOptions Optional options for preferred format and quality.
* @returns {import("../source/IIIF.js").Options} IIIF tile source ready constructor options.
* @api
*/

View File

@@ -378,6 +378,11 @@ function createStyleDefaults() {
}
/**
* @type {HTMLTextAreaElement}
*/
let TEXTAREA;
/**
* @typedef {Object} Options
@@ -877,32 +882,32 @@ class KML extends XMLFeature {
* @return {Style} style Style.
*/
function createNameStyleFunction(foundStyle, name) {
let textStyle = null;
const textOffset = [0, 0];
let textAlign = 'start';
if (foundStyle.getImage()) {
let imageSize = foundStyle.getImage().getImageSize();
const imageStyle = foundStyle.getImage();
if (imageStyle) {
let imageSize = imageStyle.getImageSize();
if (imageSize === null) {
imageSize = DEFAULT_IMAGE_STYLE_SIZE;
}
if (imageSize.length == 2) {
const imageScale = foundStyle.getImage().getScale();
// Offset the label to be centered to the right of the icon, if there is
// one.
const imageScale = imageStyle.getScale();
// Offset the label to be centered to the right of the icon,
// if there is one.
textOffset[0] = imageScale * imageSize[0] / 2;
textOffset[1] = -imageScale * imageSize[1] / 2;
textAlign = 'left';
}
}
if (foundStyle.getText() !== null) {
let textStyle = foundStyle.getText();
if (textStyle) {
// clone the text style, customizing it with name, alignments and offset.
// Note that kml does not support many text options that OpenLayers does (rotation, textBaseline).
const foundText = foundStyle.getText();
textStyle = foundText.clone();
textStyle.setFont(foundText.getFont() || DEFAULT_TEXT_STYLE.getFont());
textStyle.setScale(foundText.getScale() || DEFAULT_TEXT_STYLE.getScale());
textStyle.setFill(foundText.getFill() || DEFAULT_TEXT_STYLE.getFill());
textStyle.setStroke(foundText.getStroke() || DEFAULT_TEXT_STROKE_STYLE);
textStyle = textStyle.clone();
textStyle.setFont(textStyle.getFont() || DEFAULT_TEXT_STYLE.getFont());
textStyle.setScale(textStyle.getScale() || DEFAULT_TEXT_STYLE.getScale());
textStyle.setFill(textStyle.getFill() || DEFAULT_TEXT_STYLE.getFill());
textStyle.setStroke(textStyle.getStroke() || DEFAULT_TEXT_STROKE_STYLE);
} else {
textStyle = DEFAULT_TEXT_STYLE.clone();
}
@@ -912,6 +917,7 @@ function createNameStyleFunction(foundStyle, name) {
textStyle.setTextAlign(textAlign);
const nameStyle = new Style({
image: imageStyle,
text: textStyle
});
return nameStyle;
@@ -932,45 +938,66 @@ function createFeatureStyleFunction(style, styleUrl, defaultStyle, sharedStyles,
/**
* @param {Feature} feature feature.
* @param {number} resolution Resolution.
* @return {Array<Style>} Style.
* @return {Array<Style>|Style} Style.
*/
function(feature, resolution) {
let drawName = showPointNames;
/** @type {Style|undefined} */
let nameStyle;
let name = '';
let multiGeometryPoints = [];
if (drawName) {
const geometry = feature.getGeometry();
if (geometry) {
drawName = geometry.getType() === GeometryType.POINT;
const type = geometry.getType();
if (type === GeometryType.GEOMETRY_COLLECTION) {
multiGeometryPoints = geometry.getGeometriesArrayRecursive().filter(function(geometry) {
const type = geometry.getType();
return type === GeometryType.POINT || type === GeometryType.MULTI_POINT;
});
drawName = multiGeometryPoints.length > 0;
} else {
drawName = type === GeometryType.POINT || type === GeometryType.MULTI_POINT;
}
}
}
if (drawName) {
name = /** @type {string} */ (feature.get('name'));
drawName = drawName && !!name;
// convert any html character codes
if (drawName && name.search(/&[^&]+;/) > -1) {
if (!TEXTAREA) {
TEXTAREA = document.createElement('textarea');
}
TEXTAREA.innerHTML = name;
name = TEXTAREA.value;
}
}
let featureStyle = defaultStyle;
if (style) {
if (drawName) {
nameStyle = createNameStyleFunction(style[0], name);
return style.concat(nameStyle);
}
return style;
}
if (styleUrl) {
const foundStyle = findStyle(styleUrl, defaultStyle, sharedStyles);
if (drawName) {
nameStyle = createNameStyleFunction(foundStyle[0], name);
return foundStyle.concat(nameStyle);
}
return foundStyle;
featureStyle = style;
} else if (styleUrl) {
featureStyle = findStyle(styleUrl, defaultStyle, sharedStyles);
}
if (drawName) {
nameStyle = createNameStyleFunction(defaultStyle[0], name);
return defaultStyle.concat(nameStyle);
const nameStyle = createNameStyleFunction(featureStyle[0], name);
if (multiGeometryPoints.length > 0) {
// in multigeometries restrict the name style to points and create a
// style without image or text for geometries requiring fill or stroke
// including any polygon specific style if there is one
nameStyle.setGeometry(new GeometryCollection(multiGeometryPoints));
const baseStyle = new Style({
geometry: featureStyle[0].getGeometry(),
image: null,
fill: featureStyle[0].getFill(),
stroke: featureStyle[0].getStroke(),
text: null
});
return [nameStyle, baseStyle].concat(featureStyle.slice(1));
}
return nameStyle;
}
return defaultStyle;
return featureStyle;
}
);
}
@@ -1140,6 +1167,7 @@ function readStyleMapValue(node, objectStack) {
const ICON_STYLE_PARSERS = makeStructureNS(
NAMESPACE_URIS, {
'Icon': makeObjectPropertySetter(readIcon),
'color': makeObjectPropertySetter(readColor),
'heading': makeObjectPropertySetter(readDecimal),
'hotSpot': makeObjectPropertySetter(readVec2),
'scale': makeObjectPropertySetter(readScale)
@@ -1221,6 +1249,9 @@ function iconStyleParser(node, objectStack) {
let scale = /** @type {number|undefined} */
(object['scale']);
const color = /** @type {Array<number>|undefined} */
(object['color']);
if (drawIcon) {
if (src == DEFAULT_IMAGE_STYLE_SRC) {
size = DEFAULT_IMAGE_STYLE_SIZE;
@@ -1240,7 +1271,8 @@ function iconStyleParser(node, objectStack) {
rotation: rotation,
scale: scale,
size: size,
src: src
src: src,
color: color
});
styleObject['imageStyle'] = imageStyle;
} else {
@@ -1759,13 +1791,57 @@ function readStyle(node, objectStack) {
const textStyle = /** @type {Text} */
('textStyle' in styleObject ?
styleObject['textStyle'] : DEFAULT_TEXT_STYLE);
let strokeStyle = /** @type {Stroke} */
const strokeStyle = /** @type {Stroke} */
('strokeStyle' in styleObject ?
styleObject['strokeStyle'] : DEFAULT_STROKE_STYLE);
const outline = /** @type {boolean|undefined} */
(styleObject['outline']);
if (outline !== undefined && !outline) {
strokeStyle = null;
// if the polystyle specifies no outline two styles are needed,
// one for non-polygon geometries where linestrings require a stroke
// and one for polygons where there should be no stroke
return [
new Style({
geometry: function(feature) {
const geometry = feature.getGeometry();
const type = geometry.getType();
if (type === GeometryType.GEOMETRY_COLLECTION) {
return new GeometryCollection(
geometry.getGeometriesArrayRecursive().filter(function(geometry) {
const type = geometry.getType();
return type !== GeometryType.POLYGON && type !== GeometryType.MULTI_POLYGON;
})
);
} else if (type !== GeometryType.POLYGON && type !== GeometryType.MULTI_POLYGON) {
return geometry;
}
},
fill: fillStyle,
image: imageStyle,
stroke: strokeStyle,
text: textStyle,
zIndex: undefined // FIXME
}),
new Style({
geometry: function(feature) {
const geometry = feature.getGeometry();
const type = geometry.getType();
if (type === GeometryType.GEOMETRY_COLLECTION) {
return new GeometryCollection(
geometry.getGeometriesArrayRecursive().filter(function(geometry) {
const type = geometry.getType();
return type === GeometryType.POLYGON || type === GeometryType.MULTI_POLYGON;
})
);
} else if (type === GeometryType.POLYGON || type === GeometryType.MULTI_POLYGON) {
return geometry;
}
},
fill: fillStyle,
stroke: null,
zIndex: undefined // FIXME
})
];
}
return [new Style({
fill: fillStyle,
@@ -2372,7 +2448,7 @@ function writeIcon(node, icon, objectStack) {
// @ts-ignore
const ICON_STYLE_SEQUENCE = makeStructureNS(
NAMESPACE_URIS, [
'scale', 'heading', 'Icon', 'hotSpot'
'scale', 'heading', 'Icon', 'color', 'hotSpot'
]);
@@ -2384,6 +2460,7 @@ const ICON_STYLE_SEQUENCE = makeStructureNS(
const ICON_STYLE_SERIALIZERS = makeStructureNS(
NAMESPACE_URIS, {
'Icon': makeChildAppender(writeIcon),
'color': makeChildAppender(writeColorTextNode),
'heading': makeChildAppender(writeDecimalTextNode),
'hotSpot': makeChildAppender(writeVec2),
'scale': makeChildAppender(writeScaleTextNode)
@@ -2439,6 +2516,11 @@ function writeIconStyle(node, style, objectStack) {
properties['heading'] = rotation; // 0-360
}
const color = style.getColor();
if (color) {
properties['color'] = color;
}
const parentNode = objectStack[objectStack.length - 1].node;
const orderedKeys = ICON_STYLE_SEQUENCE[parentNode.namespaceURI];
const values = makeSequence(properties, orderedKeys);
@@ -2527,7 +2609,7 @@ function writeLineStyle(node, style, objectStack) {
const /** @type {import("../xml.js").NodeStackItem} */ context = {node: node};
const properties = {
'color': style.getColor(),
'width': style.getWidth()
'width': Number(style.getWidth()) || 1
};
const parentNode = objectStack[objectStack.length - 1].node;
const orderedKeys = LINE_STYLE_SEQUENCE[parentNode.namespaceURI];
@@ -2628,20 +2710,35 @@ function writeMultiGeometry(node, geometry, objectStack) {
const context = {node: node};
const type = geometry.getType();
/** @type {Array<import("../geom/Geometry.js").default>} */
let geometries;
let geometries = [];
/** @type {function(*, Array<*>, string=): (Node|undefined)} */
let factory;
if (type == GeometryType.GEOMETRY_COLLECTION) {
geometries = /** @type {GeometryCollection} */ (geometry).getGeometries();
if (type === GeometryType.GEOMETRY_COLLECTION) {
/** @type {GeometryCollection} */ (geometry).getGeometriesArrayRecursive().forEach(function(geometry) {
const type = geometry.getType();
if (type === GeometryType.MULTI_POINT) {
geometries = geometries.concat(/** @type {MultiPoint} */ (geometry).getPoints());
} else if (type === GeometryType.MULTI_LINE_STRING) {
geometries = geometries.concat(/** @type {MultiLineString} */ (geometry).getLineStrings());
} else if (type === GeometryType.MULTI_POLYGON) {
geometries = geometries.concat(/** @type {MultiPolygon} */ (geometry).getPolygons());
} else if (type === GeometryType.POINT
|| type === GeometryType.LINE_STRING
|| type === GeometryType.POLYGON) {
geometries.push(geometry);
} else {
assert(false, 39); // Unknown geometry type
}
});
factory = GEOMETRY_NODE_FACTORY;
} else if (type == GeometryType.MULTI_POINT) {
} else if (type === GeometryType.MULTI_POINT) {
geometries = /** @type {MultiPoint} */ (geometry).getPoints();
factory = POINT_NODE_FACTORY;
} else if (type == GeometryType.MULTI_LINE_STRING) {
} else if (type === GeometryType.MULTI_LINE_STRING) {
geometries =
(/** @type {MultiLineString} */ (geometry)).getLineStrings();
factory = LINE_STRING_NODE_FACTORY;
} else if (type == GeometryType.MULTI_POLYGON) {
} else if (type === GeometryType.MULTI_POLYGON) {
geometries =
(/** @type {MultiPolygon} */ (geometry)).getPolygons();
factory = POLYGON_NODE_FACTORY;
@@ -2756,13 +2853,61 @@ function writePlacemark(node, feature, objectStack) {
// resolution-independent here
const styles = styleFunction(feature, 0);
if (styles) {
const style = Array.isArray(styles) ? styles[0] : styles;
if (this.writeStyles_) {
properties['Style'] = style;
const styleArray = Array.isArray(styles) ? styles : [styles];
let pointStyles = styleArray;
if (feature.getGeometry()) {
pointStyles = styleArray.filter(function(style) {
const geometry = style.getGeometryFunction()(feature);
if (geometry) {
const type = geometry.getType();
if (type === GeometryType.GEOMETRY_COLLECTION) {
return /** @type {GeometryCollection} */ (geometry).getGeometriesArrayRecursive().filter(function(geometry) {
const type = geometry.getType();
return type === GeometryType.POINT || type === GeometryType.MULTI_POINT;
}).length;
}
return type === GeometryType.POINT || type === GeometryType.MULTI_POINT;
}
});
}
const textStyle = style.getText();
if (textStyle) {
properties['name'] = textStyle.getText();
if (this.writeStyles_) {
let lineStyles = styleArray;
let polyStyles = styleArray;
if (feature.getGeometry()) {
lineStyles = styleArray.filter(function(style) {
const geometry = style.getGeometryFunction()(feature);
if (geometry) {
const type = geometry.getType();
if (type === GeometryType.GEOMETRY_COLLECTION) {
return /** @type {GeometryCollection} */ (geometry).getGeometriesArrayRecursive().filter(function(geometry) {
const type = geometry.getType();
return type === GeometryType.LINE_STRING || type === GeometryType.MULTI_LINE_STRING;
}).length;
}
return type === GeometryType.LINE_STRING || type === GeometryType.MULTI_LINE_STRING;
}
});
polyStyles = styleArray.filter(function(style) {
const geometry = style.getGeometryFunction()(feature);
if (geometry) {
const type = geometry.getType();
if (type === GeometryType.GEOMETRY_COLLECTION) {
return /** @type {GeometryCollection} */ (geometry).getGeometriesArrayRecursive().filter(function(geometry) {
const type = geometry.getType();
return type === GeometryType.POLYGON || type === GeometryType.MULTI_POLYGON;
}).length;
}
return type === GeometryType.POLYGON || type === GeometryType.MULTI_POLYGON;
}
});
}
properties['Style'] = {pointStyles: pointStyles, lineStyles: lineStyles, polyStyles: polyStyles};
}
if (pointStyles.length && properties['name'] === undefined) {
const textStyle = pointStyles[0].getText();
if (textStyle) {
properties['name'] = textStyle.getText();
}
}
}
}
@@ -2838,6 +2983,17 @@ function writePrimitiveGeometry(node, geometry, objectStack) {
}
/**
* @const
* @type {Object<string, Array<string>>}
*/
// @ts-ignore
const POLY_STYLE_SEQUENCE = makeStructureNS(
NAMESPACE_URIS, [
'color', 'fill', 'outline'
]);
/**
* @const
* @type {Object<string, Object<string, import("../xml.js").Serializer>>}
@@ -2897,27 +3053,31 @@ function writePolygon(node, polygon, objectStack) {
// @ts-ignore
const POLY_STYLE_SERIALIZERS = makeStructureNS(
NAMESPACE_URIS, {
'color': makeChildAppender(writeColorTextNode)
'color': makeChildAppender(writeColorTextNode),
'fill': makeChildAppender(writeBooleanTextNode),
'outline': makeChildAppender(writeBooleanTextNode)
});
/**
* A factory for creating coordinates nodes.
* @const
* @type {function(*, Array<*>, string=): (Node|undefined)}
*/
const COLOR_NODE_FACTORY = makeSimpleNodeFactory('color');
/**
* @param {Node} node Node.
* @param {Fill} style Style.
* @param {Style} style Style.
* @param {Array<*>} objectStack Object stack.
*/
function writePolyStyle(node, style, objectStack) {
const /** @type {import("../xml.js").NodeStackItem} */ context = {node: node};
const fill = style.getFill();
const stroke = style.getStroke();
const properties = {
'color': fill ? fill.getColor() : undefined,
'fill': fill ? undefined : false,
'outline': stroke ? undefined : false
};
const parentNode = objectStack[objectStack.length - 1].node;
const orderedKeys = POLY_STYLE_SEQUENCE[parentNode.namespaceURI];
const values = makeSequence(properties, orderedKeys);
pushSerializeAndPop(context, POLY_STYLE_SERIALIZERS,
COLOR_NODE_FACTORY, [style.getColor()], objectStack);
OBJECT_PROPERTY_NODE_FACTORY, values, objectStack, orderedKeys);
}
@@ -2959,27 +3119,34 @@ const STYLE_SERIALIZERS = makeStructureNS(
/**
* @param {Node} node Node.
* @param {Style} style Style.
* @param {Object<string, Array<Style>>} styles Styles.
* @param {Array<*>} objectStack Object stack.
*/
function writeStyle(node, style, objectStack) {
function writeStyle(node, styles, objectStack) {
const /** @type {import("../xml.js").NodeStackItem} */ context = {node: node};
const properties = {};
const fillStyle = style.getFill();
const strokeStyle = style.getStroke();
const imageStyle = style.getImage();
const textStyle = style.getText();
if (imageStyle && typeof /** @type {?} */ (imageStyle).getSrc === 'function') {
properties['IconStyle'] = imageStyle;
if (styles.pointStyles.length) {
const textStyle = styles.pointStyles[0].getText();
if (textStyle) {
properties['LabelStyle'] = textStyle;
}
const imageStyle = styles.pointStyles[0].getImage();
if (imageStyle && typeof /** @type {?} */ (imageStyle).getSrc === 'function') {
properties['IconStyle'] = imageStyle;
}
}
if (textStyle) {
properties['LabelStyle'] = textStyle;
if (styles.lineStyles.length) {
const strokeStyle = styles.lineStyles[0].getStroke();
if (strokeStyle) {
properties['LineStyle'] = strokeStyle;
}
}
if (strokeStyle) {
properties['LineStyle'] = strokeStyle;
}
if (fillStyle) {
properties['PolyStyle'] = fillStyle;
if (styles.polyStyles.length) {
const strokeStyle = styles.polyStyles[0].getStroke();
if (strokeStyle && !properties['LineStyle']) {
properties['LineStyle'] = strokeStyle;
}
properties['PolyStyle'] = styles.polyStyles[0];
}
const parentNode = objectStack[objectStack.length - 1].node;
const orderedKeys = STYLE_SEQUENCE[parentNode.namespaceURI];

View File

@@ -74,7 +74,10 @@ class WMTSCapabilities extends XML {
* @inheritDoc
*/
readFromNode(node) {
const version = node.getAttribute('version').trim();
let version = node.getAttribute('version');
if (version) {
version = version.trim();
}
let WMTSCapabilityObject = this.owsParser_.readFromNode(node);
if (!WMTSCapabilityObject) {
return null;

View File

@@ -126,6 +126,23 @@ class GeometryCollection extends Geometry {
return this.geometries_;
}
/**
* @return {Array<Geometry>} Geometries.
*/
getGeometriesArrayRecursive() {
/** @type {Array<Geometry>} */
let geometriesArray = [];
const geometries = this.geometries_;
for (let i = 0, ii = geometries.length; i < ii; ++i) {
if (geometries[i].getType() === this.getType()) {
geometriesArray = geometriesArray.concat(/** @type {GeometryCollection} */ (geometries[i]).getGeometriesArrayRecursive());
} else {
geometriesArray.push(geometries[i]);
}
}
return geometriesArray;
}
/**
* @inheritDoc
*/

View File

@@ -37,7 +37,15 @@ export const MAC = ua.indexOf('macintosh') !== -1;
* @type {number}
* @api
*/
export const DEVICE_PIXEL_RATIO = window.devicePixelRatio || 1;
export const DEVICE_PIXEL_RATIO = typeof devicePixelRatio !== 'undefined' ? devicePixelRatio : 1;
/**
* The execution context is a worker with OffscreenCanvas available.
* @const
* @type {boolean}
*/
export const WORKER_OFFSCREEN_CANVAS = typeof WorkerGlobalScope !== 'undefined' && typeof OffscreenCanvas !== 'undefined' &&
self instanceof WorkerGlobalScope; //eslint-disable-line
/**
* Image.prototype.decode() is supported.

View File

@@ -13,6 +13,8 @@ import PointerInteraction, {centroid as centroidFromPointers} from './Pointer.js
* @property {import("../events/condition.js").Condition} [condition] A function that takes an {@link module:ol/MapBrowserEvent~MapBrowserEvent} and returns a boolean
* to indicate whether that event should be handled.
* Default is {@link module:ol/events/condition~noModifierKeys} and {@link module:ol/events/condition~primaryAction}.
* In addition, if there is a `tabindex` attribute on the map element,
* {@link module:ol/events/condition~focus} will also be applied.
* @property {import("../Kinetic.js").default} [kinetic] Kinetic inertia to apply to the pan.
*/

View File

@@ -110,7 +110,7 @@ import {fromUserCoordinate, getUserProjection} from '../proj.js';
* geometry is the geometry that is returned when the function is called without
* a second argument.
* @typedef {function(!SketchCoordType, import("../geom/SimpleGeometry.js").default=,
* import("../proj/Projection.js").default):
* import("../proj/Projection.js").default=):
* import("../geom/SimpleGeometry.js").default} GeometryFunction
*/
@@ -143,7 +143,13 @@ const DrawEventType = {
* @event DrawEvent#drawend
* @api
*/
DRAWEND: 'drawend'
DRAWEND: 'drawend',
/**
* Triggered upon feature draw abortion
* @event DrawEvent#drawabort
* @api
*/
DRAWABORT: 'drawabort'
};
@@ -505,7 +511,7 @@ class Draw extends PointerInteraction {
if (this.freehand_ &&
event.type === MapBrowserEventType.POINTERDRAG &&
this.sketchFeature_ !== null) {
this.addToDrawing_(event);
this.addToDrawing_(event.coordinate);
pass = false;
} else if (this.freehand_ &&
event.type === MapBrowserEventType.POINTERDOWN) {
@@ -580,12 +586,11 @@ class Draw extends PointerInteraction {
this.finishDrawing();
}
} else {
this.addToDrawing_(event);
this.addToDrawing_(event.coordinate);
}
pass = false;
} else if (this.freehand_) {
this.finishCoordinate_ = null;
this.abortDrawing_();
this.abortDrawing();
}
if (!pass && this.stopClick_) {
event.stopPropagation();
@@ -764,13 +769,12 @@ class Draw extends PointerInteraction {
/**
* Add a new coordinate to the drawing.
* @param {import("../MapBrowserEvent.js").default} event Event.
* @param {!PointCoordType} coordinate Coordinate
* @private
*/
addToDrawing_(event) {
const coordinate = event.coordinate;
addToDrawing_(coordinate) {
const geometry = this.sketchFeature_.getGeometry();
const projection = event.map.getView().getProjection();
const projection = this.getMap().getView().getProjection();
let done;
let coordinates;
if (this.mode_ === Mode.LINE_STRING) {
@@ -835,7 +839,7 @@ class Draw extends PointerInteraction {
}
if (coordinates.length === 0) {
this.finishCoordinate_ = null;
this.abortDrawing();
}
this.updateSketchFeatures_();
@@ -903,9 +907,56 @@ class Draw extends PointerInteraction {
}
/**
* Extend an existing geometry by adding additional points. This only works
* on features with `LineString` geometries, where the interaction will
* extend lines by adding points to the end of the coordinates array.
* Stop drawing without adding the sketch feature to the target layer.
* @api
*/
abortDrawing() {
const sketchFeature = this.abortDrawing_();
if (sketchFeature) {
this.dispatchEvent(new DrawEvent(DrawEventType.DRAWABORT, sketchFeature));
}
}
/**
* Append coordinates to the end of the geometry that is currently being drawn.
* This can be used when drawing LineStrings or Polygons. Coordinates will
* either be appended to the current LineString or the outer ring of the current
* Polygon.
* @param {!LineCoordType} coordinates Linear coordinates to be appended into
* the coordinate array.
* @api
*/
appendCoordinates(coordinates) {
const mode = this.mode_;
let sketchCoords = [];
if (mode === Mode.LINE_STRING) {
sketchCoords = /** @type {LineCoordType} */ this.sketchCoords_;
} else if (mode === Mode.POLYGON) {
sketchCoords = this.sketchCoords_ && this.sketchCoords_.length ? /** @type {PolyCoordType} */ (this.sketchCoords_)[0] : [];
}
// Remove last coordinate from sketch drawing (this coordinate follows cursor position)
const ending = sketchCoords.pop();
// Append coordinate list
for (let i = 0; i < coordinates.length; i++) {
this.addToDrawing_(coordinates[i]);
}
// Duplicate last coordinate for sketch drawing
this.addToDrawing_(ending);
}
/**
* Initiate draw mode by starting from an existing geometry which will
* receive new additional points. This only works on features with
* `LineString` geometries, where the interaction will extend lines by adding
* points to the end of the coordinates array.
* This will change the original feature, instead of drawing a copy.
*
* The function will dispatch a `drawstart` event.
*
* @param {!Feature<LineString>} feature Feature to be extended.
* @api
*/
@@ -948,7 +999,7 @@ class Draw extends PointerInteraction {
const map = this.getMap();
const active = this.getActive();
if (!map || !active) {
this.abortDrawing_();
this.abortDrawing();
}
this.overlay_.setMap(active ? map : null);
}

View File

@@ -14,13 +14,14 @@ import {always, primaryAction, altKeyOnly, singleClick} from '../events/conditio
import {boundingExtent, buffer as bufferExtent, createOrUpdateFromCoordinate as createExtent} from '../extent.js';
import GeometryType from '../geom/GeometryType.js';
import Point from '../geom/Point.js';
import {fromCircle} from '../geom/Polygon.js';
import PointerInteraction from './Pointer.js';
import VectorLayer from '../layer/Vector.js';
import VectorSource from '../source/Vector.js';
import VectorEventType from '../source/VectorEventType.js';
import RBush from '../structs/RBush.js';
import {createEditingStyle} from '../style/Style.js';
import {fromUserExtent, toUserExtent, fromUserCoordinate, toUserCoordinate} from '../proj.js';
import {getUserProjection, fromUserExtent, toUserExtent, fromUserCoordinate, toUserCoordinate} from '../proj.js';
/**
@@ -657,7 +658,14 @@ class Modify extends PointerInteraction {
centerSegmentData.featureSegments = featureSegments;
circumferenceSegmentData.featureSegments = featureSegments;
this.rBush_.insert(createExtent(coordinates), centerSegmentData);
this.rBush_.insert(geometry.getExtent(), circumferenceSegmentData);
let circleGeometry = /** @type {import("../geom/Geometry.js").default} */ (geometry);
const userProjection = getUserProjection();
if (userProjection && this.getMap()) {
const projection = this.getMap().getView().getProjection();
circleGeometry = circleGeometry.clone().transform(userProjection, projection);
circleGeometry = fromCircle(/** @type {import("../geom/Circle.js").default} */ (circleGeometry)).transform(projection, userProjection);
}
this.rBush_.insert(circleGeometry.getExtent(), circumferenceSegmentData);
}
/**
@@ -785,7 +793,16 @@ class Modify extends PointerInteraction {
this.changingFeature_ = false;
} else { // We're dragging the circle's circumference:
this.changingFeature_ = true;
geometry.setRadius(coordinateDistance(geometry.getCenter(), vertex));
const projection = evt.map.getView().getProjection();
let radius = coordinateDistance(fromUserCoordinate(geometry.getCenter(), projection),
fromUserCoordinate(vertex, projection));
const userProjection = getUserProjection();
if (userProjection) {
const circleGeometry = geometry.clone().transform(userProjection, projection);
circleGeometry.setRadius(radius);
radius = circleGeometry.transform(projection, userProjection).getRadius();
}
geometry.setRadius(radius);
this.changingFeature_ = false;
}
break;
@@ -898,7 +915,14 @@ class Modify extends PointerInteraction {
circumferenceSegmentData.segment[0] = coordinates;
circumferenceSegmentData.segment[1] = coordinates;
this.rBush_.update(createExtent(coordinates), centerSegmentData);
this.rBush_.update(geometry.getExtent(), circumferenceSegmentData);
let circleGeometry = geometry;
const userProjection = getUserProjection();
if (userProjection) {
const projection = evt.map.getView().getProjection();
circleGeometry = circleGeometry.clone().transform(userProjection, projection);
circleGeometry = fromCircle(circleGeometry).transform(projection, userProjection);
}
this.rBush_.update(circleGeometry.getExtent(), circumferenceSegmentData);
} else {
this.rBush_.update(boundingExtent(segmentData.segment), segmentData);
}
@@ -1249,11 +1273,15 @@ function projectedDistanceToSegmentDataSquared(pointCoordinates, segmentData, pr
const geometry = segmentData.geometry;
if (geometry.getType() === GeometryType.CIRCLE) {
const circleGeometry = /** @type {import("../geom/Circle.js").default} */ (geometry);
let circleGeometry = /** @type {import("../geom/Circle.js").default} */ (geometry);
if (segmentData.index === CIRCLE_CIRCUMFERENCE_INDEX) {
const userProjection = getUserProjection();
if (userProjection) {
circleGeometry = /** @type {import("../geom/Circle.js").default} */ (circleGeometry.clone().transform(userProjection, projection));
}
const distanceToCenterSquared =
squaredCoordinateDistance(circleGeometry.getCenter(), pointCoordinates);
squaredCoordinateDistance(circleGeometry.getCenter(), fromUserCoordinate(pointCoordinates, projection));
const distanceToCircumference =
Math.sqrt(distanceToCenterSquared) - circleGeometry.getRadius();
return distanceToCircumference * distanceToCircumference;
@@ -1280,7 +1308,12 @@ function closestOnSegmentData(pointCoordinates, segmentData, projection) {
const geometry = segmentData.geometry;
if (geometry.getType() === GeometryType.CIRCLE && segmentData.index === CIRCLE_CIRCUMFERENCE_INDEX) {
return geometry.getClosestPoint(pointCoordinates);
let circleGeometry = /** @type {import("../geom/Circle.js").default} */ (geometry);
const userProjection = getUserProjection();
if (userProjection) {
circleGeometry = /** @type {import("../geom/Circle.js").default} */ (circleGeometry.clone().transform(userProjection, projection));
}
return toUserCoordinate(circleGeometry.getClosestPoint(fromUserCoordinate(pointCoordinates, projection)), projection);
}
const coordinate = fromUserCoordinate(pointCoordinates, projection);
tempSegment[0] = fromUserCoordinate(segmentData.segment[0], projection);

View File

@@ -23,6 +23,8 @@ export const Mode = {
* takes an {@link module:ol/MapBrowserEvent~MapBrowserEvent} and returns a
* boolean to indicate whether that event should be handled. Default is
* {@link module:ol/events/condition~always}.
* In addition, if there is a `tabindex` attribute on the map element,
* {@link module:ol/events/condition~focus} will also be applied.
* @property {number} [maxDelta=1] Maximum mouse wheel delta.
* @property {number} [duration=250] Animation duration in milliseconds.
* @property {number} [timeout=80] Mouse wheel timeout duration in milliseconds.
@@ -130,7 +132,7 @@ class MouseWheelZoom extends Interaction {
* @private
* @type {number}
*/
this.trackpadDeltaPerZoom_ = 300;
this.deltaPerZoom_ = 300;
}
@@ -212,15 +214,18 @@ class MouseWheelZoom extends Interaction {
Mode.WHEEL;
}
if (this.mode_ === Mode.TRACKPAD) {
const view = map.getView();
const view = map.getView();
if (this.mode_ === Mode.TRACKPAD && !view.getConstrainResolution()) {
if (this.trackpadTimeoutId_) {
clearTimeout(this.trackpadTimeoutId_);
} else {
if (view.getAnimating()) {
view.cancelAnimations();
}
view.beginInteraction();
}
this.trackpadTimeoutId_ = setTimeout(this.endInteraction_.bind(this), this.trackpadEventGap_);
view.adjustZoom(-delta / this.trackpadDeltaPerZoom_, this.lastAnchor_);
this.trackpadTimeoutId_ = setTimeout(this.endInteraction_.bind(this), this.timeout_);
view.adjustZoom(-delta / this.deltaPerZoom_, this.lastAnchor_);
this.startTime_ = now;
return false;
}
@@ -244,8 +249,13 @@ class MouseWheelZoom extends Interaction {
if (view.getAnimating()) {
view.cancelAnimations();
}
const delta = clamp(this.totalDelta_, -this.maxDelta_, this.maxDelta_);
zoomByDelta(view, -delta, this.lastAnchor_, this.duration_);
let delta = -clamp(this.totalDelta_, -this.maxDelta_ * this.deltaPerZoom_, this.maxDelta_ * this.deltaPerZoom_) / this.deltaPerZoom_;
if (view.getConstrainResolution()) {
// view has a zoom constraint, zoom by 1
delta = delta ? delta > 0 ? 1 : -1 : 0;
}
zoomByDelta(view, delta, this.lastAnchor_, this.duration_);
this.mode_ = undefined;
this.totalDelta_ = 0;
this.lastAnchor_ = null;

View File

@@ -61,7 +61,7 @@ const SelectEventType = {
* @property {import("../style/Style.js").StyleLike} [style]
* Style for the selected features. By default the default edit style is used
* (see {@link module:ol/style}).
* If set to `false` the selected feature's style will not change.
* If set to a falsey value, the selected feature's style will not change.
* @property {import("../events/condition.js").Condition} [removeCondition] A function
* that takes an {@link module:ol/MapBrowserEvent~MapBrowserEvent} and returns a
* boolean to indicate whether that event should be handled.
@@ -133,6 +133,12 @@ class SelectEvent extends Event {
}
/**
* Original feature styles to reset to when features are no longer selected.
* @type {Object.<number, import("../style/Style.js").default|Array.<import("../style/Style.js").default>|import("../style/Style.js").StyleFunction>}
*/
const originalFeatureStyles = {};
/**
* @classdesc
@@ -161,6 +167,16 @@ class Select extends Interaction {
const options = opt_options ? opt_options : {};
/**
* @private
*/
this.boundAddFeature_ = this.addFeature_.bind(this);
/**
* @private
*/
this.boundRemoveFeature_ = this.removeFeature_.bind(this);
/**
* @private
* @type {import("../events/condition.js").Condition}
@@ -209,14 +225,6 @@ class Select extends Interaction {
*/
this.style_ = options.style !== undefined ? options.style : getDefaultStyleFunction();
/**
* An association between selected feature (key)
* and original style (value)
* @private
* @type {Object.<number, import("../style/Style.js").default|Array.<import("../style/Style.js").default>|import("../style/Style.js").StyleFunction>}
*/
this.featureStyleAssociation_ = {};
/**
* @private
* @type {import("../Collection.js").default}
@@ -251,10 +259,6 @@ class Select extends Interaction {
* @type {Object<string, import("../layer/Layer.js").default>}
*/
this.featureLayerAssociation_ = {};
const features = this.getFeatures();
features.addEventListener(CollectionEventType.ADD, this.addFeature_.bind(this));
features.addEventListener(CollectionEventType.REMOVE, this.removeFeature_.bind(this));
}
/**
@@ -319,11 +323,19 @@ class Select extends Interaction {
setMap(map) {
const currentMap = this.getMap();
if (currentMap && this.style_) {
this.features_.forEach(this.removeSelectedStyle_.bind(this));
this.features_.forEach(this.restorePreviousStyle_.bind(this));
}
super.setMap(map);
if (map && this.style_) {
this.features_.forEach(this.giveSelectedStyle_.bind(this));
if (map) {
this.features_.addEventListener(CollectionEventType.ADD, this.boundAddFeature_);
this.features_.addEventListener(CollectionEventType.REMOVE, this.boundRemoveFeature_);
if (this.style_) {
this.features_.forEach(this.applySelectedStyle_.bind(this));
}
} else {
this.features_.removeEventListener(CollectionEventType.ADD, this.boundAddFeature_);
this.features_.removeEventListener(CollectionEventType.REMOVE, this.boundRemoveFeature_);
}
}
@@ -334,7 +346,7 @@ class Select extends Interaction {
addFeature_(evt) {
const feature = evt.element;
if (this.style_) {
this.giveSelectedStyle_(feature);
this.applySelectedStyle_(feature);
}
}
@@ -345,17 +357,26 @@ class Select extends Interaction {
removeFeature_(evt) {
const feature = evt.element;
if (this.style_) {
this.removeSelectedStyle_(feature);
this.restorePreviousStyle_(feature);
}
}
/**
* @return {import("../style/Style.js").default|Array.<import("../style/Style.js").default>|import("../style/Style.js").StyleFunction|null} Select style.
*/
getStyle() {
return this.style_;
}
/**
* @param {import("../Feature.js").default} feature Feature
* @private
*/
giveSelectedStyle_(feature) {
applySelectedStyle_(feature) {
const key = getUid(feature);
this.featureStyleAssociation_[key] = feature.getStyle();
if (!(key in originalFeatureStyles)) {
originalFeatureStyles[key] = feature.getStyle();
}
feature.setStyle(this.style_);
}
@@ -363,10 +384,17 @@ class Select extends Interaction {
* @param {import("../Feature.js").default} feature Feature
* @private
*/
removeSelectedStyle_(feature) {
restorePreviousStyle_(feature) {
const key = getUid(feature);
feature.setStyle(this.featureStyleAssociation_[key]);
delete this.featureStyleAssociation_[key];
const selectInteractions = /** @type {Array<Select>} */ (this.getMap().getInteractions().getArray().filter(function(interaction) {
return interaction instanceof Select && interaction.getStyle() && interaction.getFeatures().getArray().indexOf(feature) !== -1;
}));
if (selectInteractions.length > 0) {
feature.setStyle(selectInteractions[selectInteractions.length - 1].getStyle());
} else {
feature.setStyle(originalFeatureStyles[key]);
delete originalFeatureStyles[key];
}
}
/**

View File

@@ -14,7 +14,7 @@ import PointerInteraction from './Pointer.js';
import {getValues} from '../obj.js';
import VectorEventType from '../source/VectorEventType.js';
import RBush from '../structs/RBush.js';
import {fromUserCoordinate, toUserCoordinate} from '../proj.js';
import {getUserProjection, fromUserCoordinate, toUserCoordinate} from '../proj.js';
/**
@@ -431,8 +431,13 @@ class Snap extends PointerInteraction {
} else if (this.edge_) {
const isCircle = closestSegmentData.feature.getGeometry().getType() === GeometryType.CIRCLE;
if (isCircle) {
vertex = closestOnCircle(pixelCoordinate,
/** @type {import("../geom/Circle.js").default} */ (closestSegmentData.feature.getGeometry()));
let circleGeometry = closestSegmentData.feature.getGeometry();
const userProjection = getUserProjection();
if (userProjection) {
circleGeometry = circleGeometry.clone().transform(userProjection, projection);
}
vertex = toUserCoordinate(closestOnCircle(projectedCoordinate,
/** @type {import("../geom/Circle.js").default} */ (circleGeometry)), projection);
} else {
tempSegment[0] = fromUserCoordinate(closestSegment[0], projection);
tempSegment[1] = fromUserCoordinate(closestSegment[1], projection);
@@ -482,7 +487,16 @@ class Snap extends PointerInteraction {
* @private
*/
writeCircleGeometry_(feature, geometry) {
const polygon = fromCircle(geometry);
const projection = this.getMap().getView().getProjection();
let circleGeometry = geometry;
const userProjection = getUserProjection();
if (userProjection) {
circleGeometry = /** @type {import("../geom/Circle.js").default} */ (circleGeometry.clone().transform(userProjection, projection));
}
const polygon = fromCircle(circleGeometry);
if (userProjection) {
polygon.transform(projection, userProjection);
}
const coordinates = polygon.getCoordinates()[0];
for (let i = 0, ii = coordinates.length - 1; i < ii; ++i) {
const segment = coordinates.slice(i, i + 2);

View File

@@ -19,6 +19,10 @@ import Layer from './Layer.js';
* visible.
* @property {number} [maxResolution] The maximum resolution (exclusive) below which this layer will
* be visible.
* @property {number} [minZoom] The minimum view zoom level (exclusive) above which this layer will be
* visible.
* @property {number} [maxZoom] The maximum view zoom level (inclusive) at which this layer will
* be visible.
* @property {import("../PluggableMap.js").default} [map] Sets the layer as overlay on a map. The map will not manage
* this layer in its layers collection, and the layer will be rendered on top. This is useful for
* temporary layers. The standard way to add a layer to a map and have it managed by the map is to

View File

@@ -21,6 +21,10 @@ import {assign} from '../obj.js';
* visible.
* @property {number} [maxResolution] The maximum resolution (exclusive) below which this layer will
* be visible.
* @property {number} [minZoom] The minimum view zoom level (exclusive) above which this layer will be
* visible.
* @property {number} [maxZoom] The maximum view zoom level (inclusive) at which this layer will
* be visible.
* @property {number} [preload=0] Preload. Load low-resolution tiles up to `preload` levels. `0`
* means no preloading.
* @property {import("../source/Tile.js").default} [source] Source for this layer.

View File

@@ -21,6 +21,10 @@ import {createDefaultStyle, toFunction as toStyleFunction} from '../style/Style.
* visible.
* @property {number} [maxResolution] The maximum resolution (exclusive) below which this layer will
* be visible.
* @property {number} [minZoom] The minimum view zoom level (exclusive) above which this layer will be
* visible.
* @property {number} [maxZoom] The maximum view zoom level (inclusive) at which this layer will
* be visible.
* @property {import("../render.js").OrderFunction} [renderOrder] Render order. Function to be used when sorting
* features before rendering. By default features are drawn in the order that they are created. Use
* `null` to avoid the sort, but get an undefined draw order.

View File

@@ -12,18 +12,31 @@ import VectorSource from '../source/Vector.js';
import {
equivalent as equivalentProjection,
get as getProjection,
getTransform,
transformExtent
getTransform
} from '../proj.js';
import {getCenter, intersects, equals, getIntersection, isEmpty} from '../extent.js';
import {
applyTransform,
containsCoordinate,
containsExtent,
equals,
approximatelyEquals,
getCenter,
getHeight,
getIntersection,
getWidth,
intersects,
isEmpty,
wrapX as wrapExtentX
} from '../extent.js';
import {clamp} from '../math.js';
import Style from '../style/Style.js';
import Feature from '../Feature.js';
import {bbox} from '../loadingstrategy.js';
import {meridian, parallel} from '../geom/flat/geodesic.js';
import GeometryLayout from '../geom/GeometryLayout.js';
import Point from '../geom/Point.js';
import Collection from '../Collection.js';
import {getVectorContext} from '../render.js';
import EventType from '../render/EventType.js';
/**
@@ -65,6 +78,10 @@ const INTERVALS = [
* visible.
* @property {number} [maxResolution] The maximum resolution (exclusive) below which this layer will
* be visible.
* @property {number} [minZoom] The minimum view zoom level (exclusive) above which this layer will be
* visible.
* @property {number} [maxZoom] The maximum view zoom level (inclusive) at which this layer will
* be visible.
* @property {number} [maxLines=100] The maximum number of meridians and
* parallels from the center of the map. The default value of 100 means that at
* most 200 meridians and 200 parallels will be displayed. The default value is
@@ -139,7 +156,8 @@ const INTERVALS = [
/**
* @classdesc
* Layer that renders a grid for a coordinate system.
* Layer that renders a grid for a coordinate system (currently only EPSG:4326 is supported).
* Note that the view projection must define both extent and worldExtent.
*
* @fires import("../render/Event.js").RenderEvent
* @api
@@ -203,25 +221,25 @@ class Graticule extends VectorLayer {
* @type {number}
* @private
*/
this.maxLatP_ = Infinity;
this.maxX_ = Infinity;
/**
* @type {number}
* @private
*/
this.maxLonP_ = Infinity;
this.maxY_ = Infinity;
/**
* @type {number}
* @private
*/
this.minLatP_ = -Infinity;
this.minX_ = -Infinity;
/**
* @type {number}
* @private
*/
this.minLonP_ = -Infinity;
this.minY_ = -Infinity;
/**
* @type {number}
@@ -271,6 +289,30 @@ class Graticule extends VectorLayer {
*/
this.projectionCenterLonLat_ = null;
/**
* @type {import("../coordinate.js").Coordinate}
* @private
*/
this.bottomLeft_ = null;
/**
* @type {import("../coordinate.js").Coordinate}
* @private
*/
this.bottomRight_ = null;
/**
* @type {import("../coordinate.js").Coordinate}
* @private
*/
this.topLeft_ = null;
/**
* @type {import("../coordinate.js").Coordinate}
* @private
*/
this.topRight_ = null;
/**
* @type {Array<GraticuleLabelDataType>}
* @private
@@ -379,6 +421,8 @@ class Graticule extends VectorLayer {
this.meridiansLabels_ = [];
this.parallelsLabels_ = [];
this.addEventListener(EventType.POSTRENDER, this.drawLabels_.bind(this));
}
/**
@@ -391,7 +435,7 @@ class Graticule extends VectorLayer {
this.setSource(
new VectorSource({
loader: this.loaderFunction.bind(this),
strategy: bbox,
strategy: this.strategyFunction.bind(this),
features: new Collection(),
overlaps: false,
useSpatialIndex: false,
@@ -414,6 +458,12 @@ class Graticule extends VectorLayer {
stroke: this.strokeStyle_
});
/**
* @type {?import("../extent.js").Extent}
* @private
*/
this.loadedExtent_ = null;
/**
* @type {?import("../extent.js").Extent}
*/
@@ -421,7 +471,31 @@ class Graticule extends VectorLayer {
this.setRenderOrder(null);
this.tmpExtent_ = null;
}
/**
* Strategy function for loading features based on the view's extent and
* resolution.
* @param {import("../extent.js").Extent} extent Extent.
* @param {number} resolution Resolution.
* @return {Array<import("../extent.js").Extent>} Extents.
*/
strategyFunction(extent, resolution) {
// extents may be passed in different worlds, to avoid endless loop we use only one
let realWorldExtent = extent.slice();
if (this.projection_ && this.getSource().getWrapX()) {
wrapExtentX(realWorldExtent, this.projection_);
}
if (this.loadedExtent_) {
if (approximatelyEquals(this.loadedExtent_, realWorldExtent, resolution)) {
// make sure result is exactly equal to previous extent
realWorldExtent = this.loadedExtent_.slice();
} else {
// we should not keep track of loaded extents
this.getSource().removeLoadedExtent(this.loadedExtent_);
}
}
return [realWorldExtent];
}
/**
@@ -431,16 +505,12 @@ class Graticule extends VectorLayer {
* @param {import("../proj/Projection.js").default} projection Projection
*/
loaderFunction(extent, resolution, projection) {
this.loadedExtent_ = extent;
const source = this.getSource();
// only consider the intersection between our own extent & the requested one
const layerExtent = this.getExtent() || [-Infinity, -Infinity, Infinity, Infinity];
const renderExtent = getIntersection(layerExtent, extent, this.tmpExtent_);
// we should not keep track of loaded extents
setTimeout(function() {
source.removeLoadedExtent(extent);
}, 0);
const renderExtent = getIntersection(layerExtent, extent);
if (this.renderedExtent_ && equals(this.renderedExtent_, renderExtent)) {
return;
@@ -468,10 +538,10 @@ class Graticule extends VectorLayer {
// first make sure we have enough features in the pool
let featureCount = this.meridians_.length + this.parallels_.length;
if (this.meridiansLabels_) {
featureCount += this.meridiansLabels_.length;
featureCount += this.meridians_.length;
}
if (this.parallelsLabels_) {
featureCount += this.parallelsLabels_.length;
featureCount += this.parallels_.length;
}
let feature;
@@ -498,27 +568,6 @@ class Graticule extends VectorLayer {
feature.setStyle(this.lineStyle_);
featuresColl.push(feature);
}
let labelData;
if (this.meridiansLabels_) {
for (i = 0, l = this.meridiansLabels_.length; i < l; ++i) {
labelData = this.meridiansLabels_[i];
feature = this.featurePool_[poolIndex++];
feature.setGeometry(labelData.geom);
feature.setStyle(this.lonLabelStyle_);
feature.set('graticule_label', labelData.text);
featuresColl.push(feature);
}
}
if (this.parallelsLabels_) {
for (i = 0, l = this.parallelsLabels_.length; i < l; ++i) {
labelData = this.parallelsLabels_[i];
feature = this.featurePool_[poolIndex++];
feature.setGeometry(labelData.geom);
feature.setStyle(this.latLabelStyle_);
feature.set('graticule_label', labelData.text);
featuresColl.push(feature);
}
}
}
/**
@@ -535,11 +584,15 @@ class Graticule extends VectorLayer {
const lineString = this.getMeridian_(lon, minLat, maxLat, squaredTolerance, index);
if (intersects(lineString.getExtent(), extent)) {
if (this.meridiansLabels_) {
const textPoint = this.getMeridianPoint_(lineString, extent, index);
this.meridiansLabels_[index] = {
geom: textPoint,
text: this.lonLabelFormatter_(lon)
};
const text = this.lonLabelFormatter_(lon);
if (index in this.meridiansLabels_) {
this.meridiansLabels_[index].text = text;
} else {
this.meridiansLabels_[index] = {
geom: new Point([]),
text: text
};
}
}
this.meridians_[index++] = lineString;
}
@@ -560,17 +613,101 @@ class Graticule extends VectorLayer {
const lineString = this.getParallel_(lat, minLon, maxLon, squaredTolerance, index);
if (intersects(lineString.getExtent(), extent)) {
if (this.parallelsLabels_) {
const textPoint = this.getParallelPoint_(lineString, extent, index);
this.parallelsLabels_[index] = {
geom: textPoint,
text: this.latLabelFormatter_(lat)
};
const text = this.latLabelFormatter_(lat);
if (index in this.parallelsLabels_) {
this.parallelsLabels_[index].text = text;
} else {
this.parallelsLabels_[index] = {
geom: new Point([]),
text: text
};
}
}
this.parallels_[index++] = lineString;
}
return index;
}
/**
* @param {import("../render/Event.js").default} event Render event.
* @private
*/
drawLabels_(event) {
const rotation = event.frameState.viewState.rotation;
const extent = event.frameState.extent;
const rotationCenter = getCenter(extent);
let rotationExtent = extent;
if (rotation) {
const width = getWidth(extent);
const height = getHeight(extent);
const cr = Math.abs(Math.cos(rotation));
const sr = Math.abs(Math.sin(rotation));
const unrotatedWidth = (sr * height - cr * width) / (sr * sr - cr * cr);
const unrotatedHeight = (sr * width - cr * height) / (sr * sr - cr * cr);
rotationExtent = [
rotationCenter[0] - unrotatedWidth / 2, rotationCenter[1] - unrotatedHeight / 2,
rotationCenter[0] + unrotatedWidth / 2, rotationCenter[1] + unrotatedHeight / 2
];
}
let startWorld = 0;
let endWorld = 0;
let labelsAtStart = this.latLabelPosition_ < 0.5;
const projectionExtent = this.projection_.getExtent();
const worldWidth = getWidth(projectionExtent);
if (this.getSource().getWrapX() && this.projection_.canWrapX() && !containsExtent(projectionExtent, extent)) {
startWorld = Math.floor((extent[0] - projectionExtent[0]) / worldWidth);
endWorld = Math.ceil((extent[2] - projectionExtent[2]) / worldWidth);
const inverted = Math.abs(rotation) > Math.PI / 2;
labelsAtStart = labelsAtStart !== inverted;
}
const vectorContext = getVectorContext(event);
for (let world = startWorld; world <= endWorld; ++world) {
let poolIndex = this.meridians_.length + this.parallels_.length;
let feature, index, l, textPoint;
if (this.meridiansLabels_) {
for (index = 0, l = this.meridiansLabels_.length; index < l; ++index) {
const lineString = this.meridians_[index];
if (!rotation && world === 0) {
textPoint = this.getMeridianPoint_(lineString, extent, index);
} else {
const clone = lineString.clone();
clone.translate(world * worldWidth, 0);
clone.rotate(-rotation, rotationCenter);
textPoint = this.getMeridianPoint_(clone, rotationExtent, index);
textPoint.rotate(rotation, rotationCenter);
}
feature = this.featurePool_[poolIndex++];
feature.setGeometry(textPoint);
feature.set('graticule_label', this.meridiansLabels_[index].text);
vectorContext.drawFeature(feature, this.lonLabelStyle_(feature));
}
}
if (this.parallelsLabels_) {
if (world === startWorld && labelsAtStart || world === endWorld && !labelsAtStart) {
for (index = 0, l = this.parallels_.length; index < l; ++index) {
const lineString = this.parallels_[index];
if (!rotation && world === 0) {
textPoint = this.getParallelPoint_(lineString, extent, index);
} else {
const clone = lineString.clone();
clone.translate(world * worldWidth, 0);
clone.rotate(-rotation, rotationCenter);
textPoint = this.getParallelPoint_(clone, rotationExtent, index);
textPoint.rotate(rotation, rotationCenter);
}
feature = this.featurePool_[poolIndex++];
feature.setGeometry(textPoint);
feature.set('graticule_label', this.parallelsLabels_[index].text);
vectorContext.drawFeature(feature, this.latLabelStyle_(feature));
}
}
}
}
}
/**
* @param {import("../extent.js").Extent} extent Extent.
* @param {import("../coordinate.js").Coordinate} center Center.
@@ -592,24 +729,91 @@ class Graticule extends VectorLayer {
return;
}
const centerLonLat = this.toLonLatTransform_(center);
let centerLon = centerLonLat[0];
let centerLat = centerLonLat[1];
let wrapX = false;
const projectionExtent = this.projection_.getExtent();
const worldWidth = getWidth(projectionExtent);
if (this.getSource().getWrapX() && this.projection_.canWrapX() && !containsExtent(projectionExtent, extent)) {
if (getWidth(extent) >= worldWidth) {
extent[0] = projectionExtent[0];
extent[2] = projectionExtent[2];
} else {
wrapX = true;
}
}
// Constrain the center to fit into the extent available to the graticule
const validCenterP = [
clamp(center[0], this.minX_, this.maxX_),
clamp(center[1], this.minY_, this.maxY_)
];
// Transform the center to lon lat
// Some projections may have a void area at the poles
// so replace any NaN latitudes with the min or max value closest to a pole
const centerLonLat = this.toLonLatTransform_(validCenterP);
if (isNaN(centerLonLat[1])) {
centerLonLat[1] = Math.abs(this.maxLat_) >= Math.abs(this.minLat_) ?
this.maxLat_ : this.minLat_;
}
let centerLon = clamp(centerLonLat[0], this.minLon_, this.maxLon_);
let centerLat = clamp(centerLonLat[1], this.minLat_, this.maxLat_);
const maxLines = this.maxLines_;
let cnt, idx, lat, lon;
let validExtent = [
Math.max(extent[0], this.minLonP_),
Math.max(extent[1], this.minLatP_),
Math.min(extent[2], this.maxLonP_),
Math.min(extent[3], this.maxLatP_)
];
// Limit the extent to fit into the extent available to the graticule
validExtent = transformExtent(validExtent, this.projection_, 'EPSG:4326');
const maxLat = validExtent[3];
const maxLon = validExtent[2];
const minLat = validExtent[1];
const minLon = validExtent[0];
let validExtentP = extent;
if (!wrapX) {
validExtentP = [
clamp(extent[0], this.minX_, this.maxX_),
clamp(extent[1], this.minY_, this.maxY_),
clamp(extent[2], this.minX_, this.maxX_),
clamp(extent[3], this.minY_, this.maxY_)
];
}
// Transform the extent to get the lon lat ranges for the edges of the extent
const validExtent = applyTransform(validExtentP, this.toLonLatTransform_, undefined, 8);
let maxLat = validExtent[3];
let maxLon = validExtent[2];
let minLat = validExtent[1];
let minLon = validExtent[0];
if (!wrapX) {
// Check if extremities of the world extent lie inside the extent
// (for example the pole in a polar projection)
// and extend the extent as appropriate
if (containsCoordinate(validExtentP, this.bottomLeft_)) {
minLon = this.minLon_;
minLat = this.minLat_;
}
if (containsCoordinate(validExtentP, this.bottomRight_)) {
maxLon = this.maxLon_;
minLat = this.minLat_;
}
if (containsCoordinate(validExtentP, this.topLeft_)) {
minLon = this.minLon_;
maxLat = this.maxLat_;
}
if (containsCoordinate(validExtentP, this.topRight_)) {
maxLon = this.maxLon_;
maxLat = this.maxLat_;
}
// The transformed center may also extend the lon lat ranges used for rendering
maxLat = clamp(maxLat, centerLat, this.maxLat_);
maxLon = clamp(maxLon, centerLon, this.maxLon_);
minLat = clamp(minLat, this.minLat_, centerLat);
minLon = clamp(minLon, this.minLon_, centerLon);
}
// Create meridians
@@ -619,17 +823,29 @@ class Graticule extends VectorLayer {
idx = this.addMeridian_(lon, minLat, maxLat, squaredTolerance, extent, 0);
cnt = 0;
while (lon != this.minLon_ && cnt++ < maxLines) {
lon = Math.max(lon - interval, this.minLon_);
idx = this.addMeridian_(lon, minLat, maxLat, squaredTolerance, extent, idx);
if (wrapX) {
while ((lon -= interval) >= minLon && cnt++ < maxLines) {
idx = this.addMeridian_(lon, minLat, maxLat, squaredTolerance, extent, idx);
}
} else {
while (lon != this.minLon_ && cnt++ < maxLines) {
lon = Math.max(lon - interval, this.minLon_);
idx = this.addMeridian_(lon, minLat, maxLat, squaredTolerance, extent, idx);
}
}
lon = clamp(centerLon, this.minLon_, this.maxLon_);
cnt = 0;
while (lon != this.maxLon_ && cnt++ < maxLines) {
lon = Math.min(lon + interval, this.maxLon_);
idx = this.addMeridian_(lon, minLat, maxLat, squaredTolerance, extent, idx);
if (wrapX) {
while ((lon += interval) <= maxLon && cnt++ < maxLines) {
idx = this.addMeridian_(lon, minLat, maxLat, squaredTolerance, extent, idx);
}
} else {
while (lon != this.maxLon_ && cnt++ < maxLines) {
lon = Math.min(lon + interval, this.maxLon_);
idx = this.addMeridian_(lon, minLat, maxLat, squaredTolerance, extent, idx);
}
}
this.meridians_.length = idx;
@@ -680,11 +896,13 @@ class Graticule extends VectorLayer {
/** @type {Array<number>} **/
const p2 = [];
for (let i = 0, ii = this.intervals_.length; i < ii; ++i) {
const delta = this.intervals_[i] / 2;
const delta = clamp(this.intervals_[i] / 2, 0, 90);
// Don't attempt to transform latitudes beyond the poles!
const clampedLat = clamp(centerLat, -90 + delta, 90 - delta);
p1[0] = centerLon - delta;
p1[1] = centerLat - delta;
p1[1] = clampedLat - delta;
p2[0] = centerLon + delta;
p2[1] = centerLat + delta;
p2[1] = clampedLat + delta;
this.fromLonLatTransform_(p1, p1);
this.fromLonLatTransform_(p2, p2);
const dist = Math.pow(p2[0] - p1[0], 2) + Math.pow(p2[1] - p1[1], 2);
@@ -727,19 +945,23 @@ class Graticule extends VectorLayer {
*/
getMeridianPoint_(lineString, extent, index) {
const flatCoordinates = lineString.getFlatCoordinates();
const clampedBottom = Math.max(extent[1], flatCoordinates[1]);
const clampedTop = Math.min(extent[3], flatCoordinates[flatCoordinates.length - 1]);
let bottom = 1;
let top = flatCoordinates.length - 1;
if (flatCoordinates[bottom] > flatCoordinates[top]) {
bottom = top;
top = 1;
}
const clampedBottom = Math.max(extent[1], flatCoordinates[bottom]);
const clampedTop = Math.min(extent[3], flatCoordinates[top]);
const lat = clamp(
extent[1] + Math.abs(extent[1] - extent[3]) * this.lonLabelPosition_,
clampedBottom, clampedTop);
const coordinate = [flatCoordinates[0], lat];
let point;
if (index in this.meridiansLabels_) {
point = this.meridiansLabels_[index].geom;
point.setCoordinates(coordinate);
} else {
point = new Point(coordinate);
}
const coordinate0 = flatCoordinates[bottom - 1] +
(flatCoordinates[top - 1] - flatCoordinates[bottom - 1]) * (lat - flatCoordinates[bottom]) /
(flatCoordinates[top] - flatCoordinates[bottom]);
const coordinate = [coordinate0, lat];
const point = this.meridiansLabels_[index].geom;
point.setCoordinates(coordinate);
return point;
}
@@ -783,19 +1005,23 @@ class Graticule extends VectorLayer {
*/
getParallelPoint_(lineString, extent, index) {
const flatCoordinates = lineString.getFlatCoordinates();
const clampedLeft = Math.max(extent[0], flatCoordinates[0]);
const clampedRight = Math.min(extent[2], flatCoordinates[flatCoordinates.length - 2]);
let left = 0;
let right = flatCoordinates.length - 2;
if (flatCoordinates[left] > flatCoordinates[right]) {
left = right;
right = 0;
}
const clampedLeft = Math.max(extent[0], flatCoordinates[left]);
const clampedRight = Math.min(extent[2], flatCoordinates[right]);
const lon = clamp(
extent[0] + Math.abs(extent[0] - extent[2]) * this.latLabelPosition_,
clampedLeft, clampedRight);
const coordinate = [lon, flatCoordinates[1]];
let point;
if (index in this.parallelsLabels_) {
point = this.parallelsLabels_[index].geom;
point.setCoordinates(coordinate);
} else {
point = new Point(coordinate);
}
const coordinate1 = flatCoordinates[left + 1] +
(flatCoordinates[right + 1] - flatCoordinates[left + 1]) * (lon - flatCoordinates[left]) /
(flatCoordinates[right] - flatCoordinates[left]);
const coordinate = [lon, coordinate1];
const point = this.parallelsLabels_[index].geom;
point.setCoordinates(coordinate);
return point;
}
@@ -816,23 +1042,66 @@ class Graticule extends VectorLayer {
const epsg4326Projection = getProjection('EPSG:4326');
const worldExtent = projection.getWorldExtent();
const worldExtentP = transformExtent(worldExtent, epsg4326Projection, projection);
this.maxLat_ = worldExtent[3];
this.maxLon_ = worldExtent[2];
this.minLat_ = worldExtent[1];
this.minLon_ = worldExtent[0];
this.maxLatP_ = worldExtentP[3];
this.maxLonP_ = worldExtentP[2];
this.minLatP_ = worldExtentP[1];
this.minLonP_ = worldExtentP[0];
// If the world extent crosses the dateline define a custom transform to
// return longitudes which wrap the dateline
const toLonLatTransform = getTransform(projection, epsg4326Projection);
if (this.minLon_ < this.maxLon_) {
this.toLonLatTransform_ = toLonLatTransform;
} else {
const split = this.minLon_ + this.maxLon_ / 2;
this.maxLon_ += 360;
this.toLonLatTransform_ = function(coordinates, opt_output, opt_dimension) {
const dimension = opt_dimension || 2;
const lonLatCoordinates = toLonLatTransform(coordinates, opt_output, dimension);
for (let i = 0, l = lonLatCoordinates.length; i < l; i += dimension) {
if (lonLatCoordinates[i] < split) {
lonLatCoordinates[i] += 360;
}
}
return lonLatCoordinates;
};
}
// Transform the extent to get the limits of the view projection extent
// which should be available to the graticule
this.fromLonLatTransform_ = getTransform(epsg4326Projection, projection);
const worldExtentP = applyTransform(
[this.minLon_, this.minLat_, this.maxLon_, this.maxLat_],
this.fromLonLatTransform_,
undefined,
8
);
this.toLonLatTransform_ = getTransform(projection, epsg4326Projection);
this.minX_ = worldExtentP[0];
this.maxX_ = worldExtentP[2];
this.minY_ = worldExtentP[1];
this.maxY_ = worldExtentP[3];
// Determine the view projection coordinates of the extremities of the world extent
// as these may lie inside a view extent (for example the pole in a polar projection)
this.bottomLeft_ = this.fromLonLatTransform_([this.minLon_, this.minLat_]);
this.bottomRight_ = this.fromLonLatTransform_([this.maxLon_, this.minLat_]);
this.topLeft_ = this.fromLonLatTransform_([this.minLon_, this.maxLat_]);
this.topRight_ = this.fromLonLatTransform_([this.maxLon_, this.maxLat_]);
// Transform the projection center to lon lat
// Some projections may have a void area at the poles
// so replace any NaN latitudes with the min or max value closest to a pole
this.projectionCenterLonLat_ = this.toLonLatTransform_(getCenter(projection.getExtent()));
if (isNaN(this.projectionCenterLonLat_[1])) {
this.projectionCenterLonLat_[1] = Math.abs(this.maxLat_) >= Math.abs(this.minLat_) ?
this.maxLat_ : this.minLat_;
}
this.projection_ = projection;
}

View File

@@ -33,6 +33,10 @@ import SourceState from '../source/State.js';
* visible.
* @property {number} [maxZoom] The maximum view zoom level (inclusive) at which this layer will
* be visible.
* @property {number} [minZoom] The minimum view zoom level (exclusive) above which this layer will be
* visible.
* @property {number} [maxZoom] The maximum view zoom level (inclusive) at which this layer will
* be visible.
* @property {Array<import("./Base.js").default>|import("../Collection.js").default<import("./Base.js").default>} [layers] Child layers.
*/

View File

@@ -24,6 +24,10 @@ import WebGLPointsLayerRenderer from '../renderer/webgl/PointsLayer.js';
* visible.
* @property {number} [maxResolution] The maximum resolution (exclusive) below which this layer will
* be visible.
* @property {number} [minZoom] The minimum view zoom level (exclusive) above which this layer will be
* visible.
* @property {number} [maxZoom] The maximum view zoom level (inclusive) at which this layer will
* be visible.
* @property {Array<string>} [gradient=['#00f', '#0ff', '#0f0', '#ff0', '#f00']] The color gradient
* of the heatmap, specified as an array of CSS color strings.
* @property {number} [radius=8] Radius size in pixels.

View File

@@ -9,6 +9,7 @@ import LayerProperty from './Property.js';
import {assign} from '../obj.js';
import RenderEventType from '../render/EventType.js';
import SourceState from '../source/State.js';
import {assert} from '../asserts.js';
/**
* @typedef {function(import("../PluggableMap.js").FrameState):HTMLElement} RenderFunction
@@ -30,6 +31,10 @@ import SourceState from '../source/State.js';
* visible.
* @property {number} [maxResolution] The maximum resolution (exclusive) below which this layer will
* be visible.
* @property {number} [minZoom] The minimum view zoom level (exclusive) above which this layer will be
* visible.
* @property {number} [maxZoom] The maximum view zoom level (inclusive) at which this layer will
* be visible.
* @property {import("../source/Source.js").default} [source] Source for this layer. If not provided to the constructor,
* the source can be set by calling {@link module:ol/layer/Layer#setSource layer.setSource(source)} after
* construction.
@@ -243,7 +248,13 @@ class Layer extends BaseLayer {
if (map) {
this.mapPrecomposeKey_ = listen(map, RenderEventType.PRECOMPOSE, function(evt) {
const renderEvent = /** @type {import("../render/Event.js").default} */ (evt);
renderEvent.frameState.layerStatesArray.push(this.getLayerState(false));
const layerStatesArray = renderEvent.frameState.layerStatesArray;
const layerState = this.getLayerState(false);
// A layer can only be added to the map once. Use either `layer.setMap()` or `map.addLayer()`, not both.
assert(!layerStatesArray.some(function(arrayLayerState) {
return arrayLayerState.layer === layerState.layer;
}), 67);
layerStatesArray.push(layerState);
}, this);
this.mapRenderKey_ = listen(this, EventType.CHANGE, map.render, map);
this.changed();

View File

@@ -20,6 +20,10 @@ import CanvasVectorImageLayerRenderer from '../renderer/canvas/VectorImageLayer.
* visible.
* @property {number} [maxResolution] The maximum resolution (exclusive) below which this layer will
* be visible.
* @property {number} [minZoom] The minimum view zoom level (exclusive) above which this layer will be
* visible.
* @property {number} [maxZoom] The maximum view zoom level (inclusive) at which this layer will
* be visible.
* @property {import("../render.js").OrderFunction} [renderOrder] Render order. Function to be used when sorting
* features before rendering. By default features are drawn in the order that they are created. Use
* `null` to avoid the sort, but get an undefined draw order.

View File

@@ -24,6 +24,10 @@ import {assign} from '../obj.js';
* visible.
* @property {number} [maxResolution] The maximum resolution (exclusive) below which this layer will
* be visible.
* @property {number} [minZoom] The minimum view zoom level (exclusive) above which this layer will be
* visible.
* @property {number} [maxZoom] The maximum view zoom level (inclusive) at which this layer will
* be visible.
* @property {import("../render.js").OrderFunction} [renderOrder] Render order. Function to be used when sorting
* features before rendering. By default features are drawn in the order that they are created. Use
* `null` to avoid the sort, but get an undefined draw order.

View File

@@ -23,6 +23,10 @@ import Layer from './Layer.js';
* visible.
* @property {number} [maxResolution] The maximum resolution (exclusive) below which this layer will
* be visible.
* @property {number} [minZoom] The minimum view zoom level (exclusive) above which this layer will be
* visible.
* @property {number} [maxZoom] The maximum view zoom level (inclusive) at which this layer will
* be visible.
* @property {import("../source/Vector.js").default} [source] Source.
* @property {boolean} [disableHitDetection=false] Setting this to true will provide a slight performance boost, but will
* prevent all hit detection on the layer.

View File

@@ -476,12 +476,14 @@ export function transform(coordinate, source, destination) {
* @param {import("./extent.js").Extent} extent The extent to transform.
* @param {ProjectionLike} source Source projection-like.
* @param {ProjectionLike} destination Destination projection-like.
* @param {number=} opt_stops Number of stops per side used for the transform.
* By default only the corners are used.
* @return {import("./extent.js").Extent} The transformed extent.
* @api
*/
export function transformExtent(extent, source, destination) {
export function transformExtent(extent, source, destination, opt_stops) {
const transformFunc = getTransform(source, destination);
return applyTransform(extent, transformFunc);
return applyTransform(extent, transformFunc, undefined, opt_stops);
}

View File

@@ -4,8 +4,10 @@
import {getFontParameters} from '../css.js';
import {createCanvasContext2D} from '../dom.js';
import {clear} from '../obj.js';
import {create as createTransform} from '../transform.js';
import LabelCache from './canvas/LabelCache.js';
import BaseObject from '../Object.js';
import EventTarget from '../events/Target.js';
import {WORKER_OFFSCREEN_CANVAS} from '../has.js';
import {toString} from '../transform.js';
/**
@@ -13,6 +15,12 @@ import LabelCache from './canvas/LabelCache.js';
* @property {import("../colorlike.js").ColorLike} fillStyle
*/
/**
* @typedef Label
* @property {number} width
* @property {number} height
* @property {Array<string|number>} contextInstructions
*/
/**
* @typedef {Object} FillStrokeState
@@ -164,21 +172,23 @@ export const defaultPadding = [0, 0, 0, 0];
*/
export const defaultLineWidth = 1;
/**
* @type {BaseObject}
*/
export const checkedFonts = new BaseObject();
/**
* The label cache for text rendering. To change the default cache size of 2048
* entries, use {@link module:ol/structs/LRUCache#setSize}.
* @type {LabelCache}
* Deprecated - there is no label cache any more.
* @type {?}
* @api
* @deprecated
*/
export const labelCache = new LabelCache();
/**
* @type {!Object<string, number>}
*/
export const checkedFonts = {};
export const labelCache = new EventTarget();
labelCache.setSize = function() {
console.warn('labelCache is deprecated.'); //eslint-disable-line
};
/**
* @type {CanvasRenderingContext2D}
@@ -200,9 +210,8 @@ export const textHeights = {};
* Clears the label cache when a font becomes available.
* @param {string} fontSpec CSS font spec.
*/
export const checkFont = (function() {
export const registerFont = (function() {
const retries = 100;
const checked = checkedFonts;
const size = '32px ';
const referenceFonts = ['monospace', 'serif'];
const len = referenceFonts.length;
@@ -235,19 +244,18 @@ export const checkFont = (function() {
function check() {
let done = true;
for (const font in checked) {
if (checked[font] < retries) {
const fonts = checkedFonts.getKeys();
for (let i = 0, ii = fonts.length; i < ii; ++i) {
const font = fonts[i];
if (checkedFonts.get(font) < retries) {
if (isAvailable.apply(this, font.split('\n'))) {
checked[font] = retries;
clear(textHeights);
// Make sure that loaded fonts are picked up by Safari
measureContext = null;
measureFont = undefined;
if (labelCache.getCount()) {
labelCache.clear();
}
checkedFonts.set(font, retries);
} else {
++checked[font];
checkedFonts.set(font, checkedFonts.get(font) + 1, true);
done = false;
}
}
@@ -267,10 +275,10 @@ export const checkFont = (function() {
for (let i = 0, ii = families.length; i < ii; ++i) {
const family = families[i];
const key = font.style + '\n' + font.weight + '\n' + family;
if (!(key in checked)) {
checked[key] = retries;
if (checkedFonts.get(key) === undefined) {
checkedFonts.set(key, retries, true);
if (!isAvailable(font.style, font.weight, family)) {
checked[key] = 0;
checkedFonts.set(key, 0, true);
if (interval === undefined) {
interval = setInterval(check, 32);
}
@@ -291,34 +299,40 @@ export const measureTextHeight = (function() {
*/
let div;
const heights = textHeights;
return function(font) {
let height = heights[font];
return function(fontSpec) {
let height = heights[fontSpec];
if (height == undefined) {
if (!div) {
div = document.createElement('div');
div.innerHTML = 'M';
div.style.margin = '0 !important';
div.style.padding = '0 !important';
div.style.position = 'absolute !important';
div.style.left = '-99999px !important';
if (WORKER_OFFSCREEN_CANVAS) {
const font = getFontParameters(fontSpec);
const metrics = measureText(fontSpec, 'Žg');
const lineHeight = isNaN(Number(font.lineHeight)) ? 1.2 : Number(font.lineHeight);
textHeights[fontSpec] = lineHeight * (metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent);
} else {
if (!div) {
div = document.createElement('div');
div.innerHTML = 'M';
div.style.margin = '0 !important';
div.style.padding = '0 !important';
div.style.position = 'absolute !important';
div.style.left = '-99999px !important';
}
div.style.font = fontSpec;
document.body.appendChild(div);
height = div.offsetHeight;
heights[fontSpec] = height;
document.body.removeChild(div);
}
div.style.font = font;
document.body.appendChild(div);
height = div.offsetHeight;
heights[font] = height;
document.body.removeChild(div);
}
return height;
};
})();
/**
* @param {string} font Font.
* @param {string} text Text.
* @return {number} Width.
* @return {TextMetrics} Text metrics.
*/
export function measureTextWidth(font, text) {
function measureText(font, text) {
if (!measureContext) {
measureContext = createCanvasContext2D(1, 1);
}
@@ -326,9 +340,17 @@ export function measureTextWidth(font, text) {
measureContext.font = font;
measureFont = measureContext.font;
}
return measureContext.measureText(text).width;
return measureContext.measureText(text);
}
/**
* @param {string} font Font.
* @param {string} text Text.
* @return {number} Width.
*/
export function measureTextWidth(font, text) {
return measureText(font, text).width;
}
/**
* Measure text width using a cache.
@@ -381,14 +403,11 @@ export function rotateAtOffset(context, rotation, offsetX, offsetY) {
}
export const resetTransform = createTransform();
/**
* @param {CanvasRenderingContext2D} context Context.
* @param {import("../transform.js").Transform|null} transform Transform.
* @param {number} opacity Opacity.
* @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} image Image.
* @param {Label|HTMLCanvasElement|HTMLImageElement|HTMLVideoElement} labelOrImage Label.
* @param {number} originX Origin X.
* @param {number} originY Origin Y.
* @param {number} w Width.
@@ -397,23 +416,63 @@ export const resetTransform = createTransform();
* @param {number} y Y.
* @param {number} scale Scale.
*/
export function drawImage(context,
transform, opacity, image, originX, originY, w, h, x, y, scale) {
let alpha;
if (opacity != 1) {
alpha = context.globalAlpha;
context.globalAlpha = alpha * opacity;
export function drawImageOrLabel(context,
transform, opacity, labelOrImage, originX, originY, w, h, x, y, scale) {
context.save();
if (opacity !== 1) {
context.globalAlpha *= opacity;
}
if (transform) {
context.setTransform.apply(context, transform);
}
context.drawImage(image, originX, originY, w, h, x, y, w * scale, h * scale);
if (opacity != 1) {
context.globalAlpha = alpha;
if ((/** @type {*} */ (labelOrImage).contextInstructions)) {
// label
context.translate(x, y);
context.scale(scale, scale);
executeLabelInstructions(/** @type {Label} */ (labelOrImage), context);
} else {
// image
context.drawImage(/** @type {HTMLCanvasElement|HTMLImageElement|HTMLVideoElement} */ (labelOrImage), originX, originY, w, h, x, y, w * scale, h * scale);
}
if (transform) {
context.setTransform.apply(context, resetTransform);
context.restore();
}
/**
* @param {Label} label Label.
* @param {CanvasRenderingContext2D} context Context.
*/
function executeLabelInstructions(label, context) {
const contextInstructions = label.contextInstructions;
for (let i = 0, ii = contextInstructions.length; i < ii; i += 2) {
if (Array.isArray(contextInstructions[i + 1])) {
context[contextInstructions[i]].apply(context, contextInstructions[i + 1]);
} else {
context[contextInstructions[i]] = contextInstructions[i + 1];
}
}
}
/**
* @type {HTMLCanvasElement}
* @private
*/
let createTransformStringCanvas = null;
/**
* @param {import("../transform.js").Transform} transform Transform.
* @return {string} CSS transform.
*/
export function createTransformString(transform) {
if (WORKER_OFFSCREEN_CANVAS) {
return toString(transform);
} else {
if (!createTransformStringCanvas) {
createTransformStringCanvas = createCanvasContext2D(1, 1).canvas;
}
createTransformStringCanvas.style.transform = toString(transform);
return createTransformStringCanvas.style.transform;
}
}

View File

@@ -7,7 +7,7 @@ import {createEmpty, createOrUpdate,
import {lineStringLength} from '../../geom/flat/length.js';
import {drawTextOnPath} from '../../geom/flat/textpath.js';
import {transform2D} from '../../geom/flat/transform.js';
import {drawImage, defaultPadding, defaultTextBaseline} from '../canvas.js';
import {drawImageOrLabel, defaultPadding, defaultTextBaseline} from '../canvas.js';
import CanvasInstruction from './Instruction.js';
import {TEXT_ALIGN} from './TextBuilder.js';
import {
@@ -16,9 +16,9 @@ import {
apply as applyTransform,
setFromArray as transformSetFromArray
} from '../../transform.js';
import {createCanvasContext2D} from '../../dom.js';
import {labelCache, defaultTextAlign, measureTextHeight, measureAndCacheTextWidth, measureTextWidths} from '../canvas.js';
import {defaultTextAlign, measureTextHeight, measureAndCacheTextWidth, measureTextWidths} from '../canvas.js';
import RBush from 'rbush/rbush.js';
import {WORKER_OFFSCREEN_CANVAS} from '../../has.js';
/**
@@ -152,6 +152,12 @@ class Executor {
* @type {Object<string, Object<string, number>>}
*/
this.widths_ = {};
/**
* @private
* @type {Object<string, import("../canvas.js").Label>}
*/
this.labels_ = {};
}
/**
@@ -159,69 +165,73 @@ class Executor {
* @param {string} textKey Text style key.
* @param {string} fillKey Fill style key.
* @param {string} strokeKey Stroke style key.
* @return {HTMLCanvasElement} Image.
* @return {import("../canvas.js").Label} Label.
*/
getTextImage(text, textKey, fillKey, strokeKey) {
let label;
const key = strokeKey + textKey + text + fillKey + this.pixelRatio;
createLabel(text, textKey, fillKey, strokeKey) {
const key = text + textKey + fillKey + strokeKey;
if (this.labels_[key]) {
return this.labels_[key];
}
const strokeState = strokeKey ? this.strokeStates[strokeKey] : null;
const fillState = fillKey ? this.fillStates[fillKey] : null;
const textState = this.textStates[textKey];
const pixelRatio = this.pixelRatio;
const scale = textState.scale * pixelRatio;
const align = TEXT_ALIGN[textState.textAlign || defaultTextAlign];
const strokeWidth = strokeKey && strokeState.lineWidth ? strokeState.lineWidth : 0;
if (!labelCache.containsKey(key)) {
const strokeState = strokeKey ? this.strokeStates[strokeKey] : null;
const fillState = fillKey ? this.fillStates[fillKey] : null;
const textState = this.textStates[textKey];
const pixelRatio = this.pixelRatio;
const scale = textState.scale * pixelRatio;
const align = TEXT_ALIGN[textState.textAlign || defaultTextAlign];
const strokeWidth = strokeKey && strokeState.lineWidth ? strokeState.lineWidth : 0;
const lines = text.split('\n');
const numLines = lines.length;
const widths = [];
const width = measureTextWidths(textState.font, lines, widths);
const lineHeight = measureTextHeight(textState.font);
const height = lineHeight * numLines;
const renderWidth = width + strokeWidth;
const context = createCanvasContext2D(
// make canvas 2 pixels wider to account for italic text width measurement errors
Math.ceil((renderWidth + 2) * scale),
Math.ceil((height + strokeWidth) * scale));
label = context.canvas;
labelCache.set(key, label);
if (scale != 1) {
context.scale(scale, scale);
}
context.font = textState.font;
if (strokeKey) {
context.strokeStyle = strokeState.strokeStyle;
context.lineWidth = strokeWidth;
context.lineCap = strokeState.lineCap;
context.lineJoin = strokeState.lineJoin;
context.miterLimit = strokeState.miterLimit;
if (context.setLineDash && strokeState.lineDash.length) {
context.setLineDash(strokeState.lineDash);
context.lineDashOffset = strokeState.lineDashOffset;
}
}
if (fillKey) {
context.fillStyle = fillState.fillStyle;
}
context.textBaseline = 'middle';
context.textAlign = 'center';
const leftRight = (0.5 - align);
const x = align * renderWidth + leftRight * strokeWidth;
let i;
if (strokeKey) {
for (i = 0; i < numLines; ++i) {
context.strokeText(lines[i], x + leftRight * widths[i], 0.5 * (strokeWidth + lineHeight) + i * lineHeight);
}
}
if (fillKey) {
for (i = 0; i < numLines; ++i) {
context.fillText(lines[i], x + leftRight * widths[i], 0.5 * (strokeWidth + lineHeight) + i * lineHeight);
}
const lines = text.split('\n');
const numLines = lines.length;
const widths = [];
const width = measureTextWidths(textState.font, lines, widths);
const lineHeight = measureTextHeight(textState.font);
const height = lineHeight * numLines;
const renderWidth = width + strokeWidth;
const contextInstructions = [];
/** @type {import("../canvas.js").Label} */
const label = {
// make canvas 2 pixels wider to account for italic text width measurement errors
width: Math.ceil((renderWidth + 2) * scale),
height: Math.ceil((height + strokeWidth) * scale),
contextInstructions: contextInstructions
};
if (scale != 1) {
contextInstructions.push('scale', [scale, scale]);
}
contextInstructions.push('font', textState.font);
if (strokeKey) {
contextInstructions.push('strokeStyle', strokeState.strokeStyle);
contextInstructions.push('lineWidth', strokeWidth);
contextInstructions.push('lineCap', strokeState.lineCap);
contextInstructions.push('lineJoin', strokeState.lineJoin);
contextInstructions.push('miterLimit', strokeState.miterLimit);
// eslint-disable-next-line
const Context = WORKER_OFFSCREEN_CANVAS ? OffscreenCanvasRenderingContext2D : CanvasRenderingContext2D;
if (Context.prototype.setLineDash) {
contextInstructions.push('setLineDash', [strokeState.lineDash]);
contextInstructions.push('lineDashOffset', strokeState.lineDashOffset);
}
}
return labelCache.get(key, this);
if (fillKey) {
contextInstructions.push('fillStyle', fillState.fillStyle);
}
contextInstructions.push('textBaseline', 'middle');
contextInstructions.push('textAlign', 'center');
const leftRight = (0.5 - align);
const x = align * renderWidth + leftRight * strokeWidth;
let i;
if (strokeKey) {
for (i = 0; i < numLines; ++i) {
contextInstructions.push('strokeText', [lines[i], x + leftRight * widths[i], 0.5 * (strokeWidth + lineHeight) + i * lineHeight]);
}
}
if (fillKey) {
for (i = 0; i < numLines; ++i) {
contextInstructions.push('fillText', [lines[i], x + leftRight * widths[i], 0.5 * (strokeWidth + lineHeight) + i * lineHeight]);
}
}
this.labels_[key] = label;
return label;
}
/**
@@ -254,7 +264,7 @@ class Executor {
* @param {CanvasRenderingContext2D} context Context.
* @param {number} x X.
* @param {number} y Y.
* @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} image Image.
* @param {import("../canvas.js").Label|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} imageOrLabel Image.
* @param {number} anchorX Anchor X.
* @param {number} anchorY Anchor Y.
* @param {import("../canvas.js").DeclutterGroup} declutterGroup Declutter group.
@@ -270,11 +280,11 @@ class Executor {
* @param {Array<*>} fillInstruction Fill instruction.
* @param {Array<*>} strokeInstruction Stroke instruction.
*/
replayImage_(
replayImageOrLabel_(
context,
x,
y,
image,
imageOrLabel,
anchorX,
anchorY,
declutterGroup,
@@ -296,8 +306,8 @@ class Executor {
x -= anchorX;
y -= anchorY;
const w = (width + originX > image.width) ? image.width - originX : width;
const h = (height + originY > image.height) ? image.height - originY : height;
const w = (width + originX > imageOrLabel.width) ? imageOrLabel.width - originX : width;
const h = (height + originY > imageOrLabel.height) ? imageOrLabel.height - originY : height;
const boxW = padding[3] + w * scale + padding[1];
const boxH = padding[0] + h * scale + padding[2];
const boxX = x - padding[3];
@@ -351,11 +361,11 @@ class Executor {
}
extend(declutterGroup, tmpExtent);
const declutterArgs = intersects ?
[context, transform ? transform.slice(0) : null, opacity, image, originX, originY, w, h, x, y, scale] :
[context, transform ? transform.slice(0) : null, opacity, imageOrLabel, originX, originY, w, h, x, y, scale] :
null;
if (declutterArgs) {
if (fillStroke) {
declutterArgs.push(fillInstruction, strokeInstruction, p1, p2, p3, p4);
declutterArgs.push(fillInstruction, strokeInstruction, p1.slice(0), p2.slice(0), p3.slice(0), p4.slice(0));
}
declutterGroup.push(declutterArgs);
}
@@ -365,7 +375,7 @@ class Executor {
/** @type {Array<*>} */ (fillInstruction),
/** @type {Array<*>} */ (strokeInstruction));
}
drawImage(context, transform, opacity, image, originX, originY, w, h, x, y, scale);
drawImageOrLabel(context, transform, opacity, imageOrLabel, originX, originY, w, h, x, y, scale);
}
}
@@ -440,7 +450,7 @@ class Executor {
declutterData[13], declutterData[14], declutterData[15], declutterData[16],
declutterData[11], declutterData[12]);
}
drawImage.apply(undefined, declutterData);
drawImageOrLabel.apply(undefined, declutterData);
if (currentAlpha !== opacity) {
context.globalAlpha = currentAlpha;
}
@@ -459,12 +469,12 @@ class Executor {
* @param {string} textKey The key of the text state.
* @param {string} strokeKey The key for the stroke state.
* @param {string} fillKey The key for the fill state.
* @return {{label: HTMLCanvasElement, anchorX: number, anchorY: number}} The text image and its anchor.
* @return {{label: import("../canvas.js").Label, anchorX: number, anchorY: number}} The text image and its anchor.
*/
drawTextImageWithPointPlacement_(text, textKey, strokeKey, fillKey) {
drawLabelWithPointPlacement_(text, textKey, strokeKey, fillKey) {
const textState = this.textStates[textKey];
const label = this.getTextImage(text, textKey, fillKey, strokeKey);
const label = this.createLabel(text, textKey, fillKey, strokeKey);
const strokeState = this.strokeStates[strokeKey];
const pixelRatio = this.pixelRatio;
@@ -472,7 +482,7 @@ class Executor {
const baseline = TEXT_ALIGN[textState.textBaseline || defaultTextBaseline];
const strokeWidth = strokeState && strokeState.lineWidth ? strokeState.lineWidth : 0;
// Remove the 2 pixels we added in getTextImage() for the anchor
// Remove the 2 pixels we added in createLabel() for the anchor
const width = label.width / pixelRatio - 2 * textState.scale;
const anchorX = align * width + 2 * (0.5 - align) * strokeWidth;
const anchorY = baseline * label.height / pixelRatio + 2 * (0.5 - baseline) * strokeWidth;
@@ -637,7 +647,7 @@ class Executor {
textKey = /** @type {string} */ (instruction[19]);
strokeKey = /** @type {string} */ (instruction[20]);
fillKey = /** @type {string} */ (instruction[21]);
const labelWithAnchor = this.drawTextImageWithPointPlacement_(text, textKey, strokeKey, fillKey);
const labelWithAnchor = this.drawLabelWithPointPlacement_(text, textKey, strokeKey, fillKey);
image = labelWithAnchor.label;
instruction[3] = image;
const textOffsetX = /** @type {number} */ (instruction[22]);
@@ -690,7 +700,7 @@ class Executor {
}
declutterGroup = declutterGroups[index];
}
this.replayImage_(context,
this.replayImageOrLabel_(context,
pixelCoordinates[d], pixelCoordinates[d + 1], image, anchorX, anchorY,
declutterGroup, height, opacity, originX, originY, rotation, scale,
snapToPixel, width, padding,
@@ -747,10 +757,10 @@ class Executor {
for (c = 0, cc = parts.length; c < cc; ++c) {
part = parts[c]; // x, y, anchorX, rotation, chunk
chars = /** @type {string} */ (part[4]);
label = this.getTextImage(chars, textKey, '', strokeKey);
label = this.createLabel(chars, textKey, '', strokeKey);
anchorX = /** @type {number} */ (part[2]) + strokeWidth;
anchorY = baseline * label.height + (0.5 - baseline) * 2 * strokeWidth - offsetY;
this.replayImage_(context,
this.replayImageOrLabel_(context,
/** @type {number} */ (part[0]), /** @type {number} */ (part[1]), label,
anchorX, anchorY, declutterGroup, label.height, 1, 0, 0,
/** @type {number} */ (part[3]), pixelRatioScale, false, label.width,
@@ -761,10 +771,10 @@ class Executor {
for (c = 0, cc = parts.length; c < cc; ++c) {
part = parts[c]; // x, y, anchorX, rotation, chunk
chars = /** @type {string} */ (part[4]);
label = this.getTextImage(chars, textKey, fillKey, '');
label = this.createLabel(chars, textKey, fillKey, '');
anchorX = /** @type {number} */ (part[2]);
anchorY = baseline * label.height - offsetY;
this.replayImage_(context,
this.replayImageOrLabel_(context,
/** @type {number} */ (part[0]), /** @type {number} */ (part[1]), label,
anchorX, anchorY, declutterGroup, label.height, 1, 0, 0,
/** @type {number} */ (part[3]), pixelRatioScale, false, label.width,

View File

@@ -1,20 +0,0 @@
import LRUCache from '../../structs/LRUCache.js';
/**
* @module ol/render/canvas/LabelCache
*/
/**
* @classdesc
* Cache of pre-rendered labels.
*/
class LabelCache extends LRUCache {
expireCache() {
while (this.canExpireCache()) {
this.pop();
}
}
}
export default LabelCache;

View File

@@ -6,7 +6,7 @@ import {asColorLike} from '../../colorlike.js';
import {intersects} from '../../extent.js';
import {matchingChunk} from '../../geom/flat/straightchunk.js';
import GeometryType from '../../geom/GeometryType.js';
import {labelCache, defaultTextAlign, defaultPadding, defaultLineCap, defaultLineDashOffset, defaultLineDash, defaultLineJoin, defaultFillStyle, checkFont, defaultFont, defaultLineWidth, defaultMiterLimit, defaultStrokeStyle, defaultTextBaseline} from '../canvas.js';
import {defaultTextAlign, defaultPadding, defaultLineCap, defaultLineDashOffset, defaultLineDash, defaultLineJoin, defaultFillStyle, registerFont, defaultFont, defaultLineWidth, defaultMiterLimit, defaultStrokeStyle, defaultTextBaseline} from '../canvas.js';
import CanvasInstruction from './Instruction.js';
import CanvasBuilder from './Builder.js';
import TextPlacement from '../../style/TextPlacement.js';
@@ -138,7 +138,6 @@ class CanvasTextBuilder extends CanvasBuilder {
*/
finish() {
const instructions = super.finish();
labelCache.expireCache();
instructions.textStates = this.textStates;
instructions.fillStates = this.fillStates;
instructions.strokeStates = this.strokeStates;
@@ -432,7 +431,7 @@ class CanvasTextBuilder extends CanvasBuilder {
textState = this.textState_;
const font = textStyle.getFont() || defaultFont;
checkFont(font);
registerFont(font);
const textScale = textStyle.getScale();
textState.overflow = textStyle.getOverflow();
textState.font = font;

View File

@@ -33,7 +33,7 @@ export function createHitDetectionImageData(size, transforms, features, styleFun
const renderer = new CanvasImmediateRenderer(context, 0.5, extent, null, rotation);
const featureCount = features.length;
// Stretch hit detection index to use the whole available color range
const indexFactor = Math.ceil((256 * 256 * 256 - 1) / featureCount);
const indexFactor = Math.floor((256 * 256 * 256 - 1) / featureCount);
const featuresByZIndex = {};
for (let i = 1; i <= featureCount; ++i) {
const feature = features[i - 1];
@@ -65,6 +65,10 @@ export function createHitDetectionImageData(size, transforms, features, styleFun
const image = originalStyle.getImage();
if (image) {
const imgSize = image.getImageSize();
if (!imgSize) {
continue;
}
const canvas = document.createElement('canvas');
canvas.width = imgSize[0];
canvas.height = imgSize[1];
@@ -121,6 +125,7 @@ export function createHitDetectionImageData(size, transforms, features, styleFun
}
}
}
document.body.appendChild(context.canvas);
return context.getImageData(0, 0, canvas.width, canvas.height);
}
@@ -141,7 +146,7 @@ export function hitDetect(pixel, features, imageData) {
const g = imageData.data[index + 1];
const b = imageData.data[index + 2];
const i = b + (256 * (g + (256 * r)));
const indexFactor = Math.ceil((256 * 256 * 256 - 1) / features.length);
const indexFactor = Math.floor((256 * 256 * 256 - 1) / features.length);
if (i && i % indexFactor === 0) {
resultFeatures.push(features[i / indexFactor - 1]);
}

View File

@@ -8,9 +8,9 @@ import RenderEventType from '../render/EventType.js';
import MapRenderer from './Map.js';
import SourceState from '../source/State.js';
import {replaceChildren} from '../dom.js';
import {labelCache} from '../render/canvas.js';
import EventType from '../events/EventType.js';
import {listen, unlistenByKey} from '../events.js';
import {checkedFonts} from '../render/canvas.js';
import ObjectEventType from '../ObjectEventType.js';
/**
@@ -29,7 +29,7 @@ class CompositeMapRenderer extends MapRenderer {
/**
* @type {import("../events.js").EventsKey}
*/
this.labelCacheKey_ = listen(labelCache, EventType.CLEAR, map.redrawText.bind(map));
this.fontChangeListenerKey_ = listen(checkedFonts, ObjectEventType.PROPERTYCHANGE, map.redrawText.bind(map));
/**
* @private
@@ -73,7 +73,7 @@ class CompositeMapRenderer extends MapRenderer {
}
disposeInternal() {
unlistenByKey(this.labelCacheKey_);
unlistenByKey(this.fontChangeListenerKey_);
this.element_.parentNode.removeChild(this.element_);
super.disposeInternal();
}

View File

@@ -9,6 +9,7 @@ import {inView} from '../layer/Layer.js';
import {shared as iconImageCache} from '../style/IconImageCache.js';
import {compose as composeTransform, makeInverse} from '../transform.js';
import {renderDeclutterItems} from '../render.js';
import {wrapX} from '../coordinate.js';
/**
* @abstract
@@ -102,19 +103,12 @@ class MapRenderer extends Disposable {
const projection = viewState.projection;
let translatedCoordinate = coordinate;
const translatedCoordinate = wrapX(coordinate.slice(), projection);
const offsets = [[0, 0]];
if (projection.canWrapX()) {
if (projection.canWrapX() && checkWrapped) {
const projectionExtent = projection.getExtent();
const worldWidth = getWidth(projectionExtent);
const x = coordinate[0];
if (x < projectionExtent[0] || x > projectionExtent[2]) {
const worldsAway = Math.ceil((projectionExtent[0] - x) / worldWidth);
translatedCoordinate = [x + worldWidth * worldsAway, coordinate[1]];
}
if (checkWrapped) {
offsets.push([-worldWidth, 0], [worldWidth, 0]);
}
offsets.push([-worldWidth, 0], [worldWidth, 0]);
}
const layerStates = frameState.layerStatesArray;

View File

@@ -8,6 +8,7 @@ import {fromUserExtent} from '../../proj.js';
import {getIntersection, isEmpty} from '../../extent.js';
import CanvasLayerRenderer from './Layer.js';
import {compose as composeTransform, makeInverse} from '../../transform.js';
import {createTransformString} from '../../render/canvas.js';
/**
* @classdesc
@@ -55,16 +56,20 @@ class CanvasImageLayerRenderer extends CanvasLayerRenderer {
}
if (!hints[ViewHint.ANIMATING] && !hints[ViewHint.INTERACTING] && !isEmpty(renderedExtent)) {
let projection = viewState.projection;
if (!ENABLE_RASTER_REPROJECTION) {
const sourceProjection = imageSource.getProjection();
if (sourceProjection) {
projection = sourceProjection;
if (imageSource) {
let projection = viewState.projection;
if (!ENABLE_RASTER_REPROJECTION) {
const sourceProjection = imageSource.getProjection();
if (sourceProjection) {
projection = sourceProjection;
}
}
}
const image = imageSource.getImage(renderedExtent, viewResolution, pixelRatio, projection);
if (image && this.loadImage(image)) {
this.image_ = image;
const image = imageSource.getImage(renderedExtent, viewResolution, pixelRatio, projection);
if (image && this.loadImage(image)) {
this.image_ = image;
}
} else {
this.image_ = null;
}
}
@@ -105,7 +110,7 @@ class CanvasImageLayerRenderer extends CanvasLayerRenderer {
);
makeInverse(this.inversePixelTransform, this.pixelTransform);
const canvasTransform = this.createTransformString(this.pixelTransform);
const canvasTransform = createTransformString(this.pixelTransform);
this.useContainer(target, canvasTransform, layerState.opacity);

View File

@@ -7,7 +7,7 @@ import RenderEvent from '../../render/Event.js';
import RenderEventType from '../../render/EventType.js';
import {rotateAtOffset} from '../../render/canvas.js';
import LayerRenderer from '../Layer.js';
import {create as createTransform, apply as applyTransform, compose as composeTransform, toString} from '../../transform.js';
import {create as createTransform, apply as applyTransform, compose as composeTransform} from '../../transform.js';
/**
* @abstract
@@ -69,12 +69,6 @@ class CanvasLayerRenderer extends LayerRenderer {
*/
this.containerReused = false;
/**
* @type {HTMLCanvasElement}
* @private
*/
this.createTransformStringCanvas_ = createCanvasContext2D(1, 1).canvas;
}
/**
@@ -269,15 +263,7 @@ class CanvasLayerRenderer extends LayerRenderer {
return data;
}
/**
* @param {import("../../transform.js").Transform} transform Transform.
* @return {string} CSS transform.
*/
createTransformString(transform) {
this.createTransformStringCanvas_.style.transform = toString(transform);
return this.createTransformStringCanvas_.style.transform;
}
}
export default CanvasLayerRenderer;

View File

@@ -9,6 +9,7 @@ import {createEmpty, equals, getIntersection, getTopLeft} from '../../extent.js'
import CanvasLayerRenderer from './Layer.js';
import {apply as applyTransform, compose as composeTransform, makeInverse} from '../../transform.js';
import {numberSafeCompareFunction} from '../../array.js';
import {createTransformString} from '../../render/canvas.js';
/**
* @classdesc
@@ -243,7 +244,7 @@ class CanvasTileLayerRenderer extends CanvasLayerRenderer {
-width / 2, -height / 2
);
const canvasTransform = this.createTransformString(this.pixelTransform);
const canvasTransform = createTransformString(this.pixelTransform);
this.useContainer(target, canvasTransform, layerState.opacity);
const context = this.context;

View File

@@ -105,6 +105,7 @@ class CanvasVectorImageLayerRenderer extends CanvasImageLayerRenderer {
const context = vectorRenderer.context;
const imageFrameState = /** @type {import("../../PluggableMap.js").FrameState} */ (assign({}, frameState, {
declutterItems: [],
extent: renderedExtent,
size: [width, height],
viewState: /** @type {import("../../View.js").State} */ (assign({}, frameState.viewState, {
rotation: 0

View File

@@ -3,7 +3,8 @@
*/
import {getUid} from '../../util.js';
import ViewHint from '../../ViewHint.js';
import {buffer, createEmpty, containsExtent, getWidth, intersects as intersectsExtent} from '../../extent.js';
import {buffer, createEmpty, containsExtent, getWidth, intersects as intersectsExtent, wrapX as wrapExtentX} from '../../extent.js';
import {wrapX as wrapCoordinateX} from '../../coordinate.js';
import {fromUserExtent, toUserExtent, getUserProjection, getTransformFromProjections} from '../../proj.js';
import CanvasBuilderGroup from '../../render/canvas/BuilderGroup.js';
import ExecutorGroup, {replayDeclutter} from '../../render/canvas/ExecutorGroup.js';
@@ -361,9 +362,10 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer {
const center = viewState.center.slice();
const extent = buffer(frameStateExtent,
vectorLayerRenderBuffer * resolution);
const projectionExtent = viewState.projection.getExtent();
const loadExtents = [extent.slice()];
const projectionExtent = projection.getExtent();
if (vectorSource.getWrapX() && viewState.projection.canWrapX() &&
if (vectorSource.getWrapX() && projection.canWrapX() &&
!containsExtent(projectionExtent, frameState.extent)) {
// For the replay group, we need an extent that intersects the real world
// (-180° to +180°). To support geometries in a coordinate range from -540°
@@ -374,8 +376,14 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer {
const gutter = Math.max(getWidth(extent) / 2, worldWidth);
extent[0] = projectionExtent[0] - gutter;
extent[2] = projectionExtent[2] + gutter;
const worldsAway = Math.floor((center[0] - projectionExtent[0]) / worldWidth);
center[0] -= (worldsAway * worldWidth);
wrapCoordinateX(center, projection);
const loadExtent = wrapExtentX(loadExtents[0], projection);
// If the extent crosses the date line, we load data for both edges of the worlds
if (loadExtent[0] < projectionExtent[0] && loadExtent[2] < projectionExtent[2]) {
loadExtents.push([loadExtent[0] + worldWidth, loadExtent[1], loadExtent[2] + worldWidth, loadExtent[3]]);
} else if (loadExtent[0] > projectionExtent[0] && loadExtent[2] > projectionExtent[2]) {
loadExtents.push([loadExtent[0] - worldWidth, loadExtent[1], loadExtent[2] - worldWidth, loadExtent[3]]);
}
}
if (!this.dirty_ &&
@@ -398,10 +406,14 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer {
const userProjection = getUserProjection();
let userTransform;
if (userProjection) {
vectorSource.loadFeatures(toUserExtent(extent, projection), resolution, userProjection);
for (let i = 0, ii = loadExtents.length; i < ii; ++i) {
vectorSource.loadFeatures(toUserExtent(loadExtents[i], projection), resolution, userProjection);
}
userTransform = getTransformFromProjections(userProjection, projection);
} else {
vectorSource.loadFeatures(extent, resolution, projection);
for (let i = 0, ii = loadExtents.length; i < ii; ++i) {
vectorSource.loadFeatures(loadExtents[i], resolution, projection);
}
}
const squaredTolerance = getSquaredRenderTolerance(resolution, pixelRatio);
@@ -457,7 +469,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer {
* @param {number} squaredTolerance Squared render tolerance.
* @param {import("../../style/Style.js").default|Array<import("../../style/Style.js").default>} styles The style or array of styles.
* @param {import("../../render/canvas/BuilderGroup.js").default} builderGroup Builder group.
* @param {import("../../proj.js").TransformFunction} opt_transform Transform from user to view projection.
* @param {import("../../proj.js").TransformFunction=} opt_transform Transform from user to view projection.
* @return {boolean} `true` if an image is loading.
*/
renderFeature(feature, squaredTolerance, styles, builderGroup, opt_transform) {

View File

@@ -6,7 +6,7 @@ import TileState from '../../TileState.js';
import ViewHint from '../../ViewHint.js';
import {listen, unlistenByKey} from '../../events.js';
import EventType from '../../events/EventType.js';
import {buffer, containsCoordinate, equals, getIntersection, intersects, containsExtent, getWidth, getTopLeft} from '../../extent.js';
import {buffer, containsCoordinate, equals, getIntersection, intersects, containsExtent, getTopLeft} from '../../extent.js';
import VectorTileRenderType from '../../layer/VectorTileRenderType.js';
import ReplayType from '../../render/canvas/BuilderType.js';
import CanvasBuilderGroup from '../../render/canvas/BuilderGroup.js';
@@ -25,6 +25,7 @@ import {
import CanvasExecutorGroup, {replayDeclutter} from '../../render/canvas/ExecutorGroup.js';
import {clear} from '../../obj.js';
import {createHitDetectionImageData, hitDetect} from '../../render/canvas/hitdetect.js';
import {wrapX} from '../../coordinate.js';
/**
@@ -353,9 +354,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
if (tile.getState() === TileState.LOADED && tile.hifi) {
const extent = tileGrid.getTileCoordExtent(tile.tileCoord);
if (source.getWrapX() && projection.canWrapX() && !containsExtent(projectionExtent, extent)) {
const worldWidth = getWidth(projectionExtent);
const worldsAway = Math.floor((coordinate[0] - projectionExtent[0]) / worldWidth);
coordinate[0] -= (worldsAway * worldWidth);
wrapX(coordinate, projection);
}
break;
}

View File

@@ -290,7 +290,8 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
this.sourceListenKeys_ = [
listen(source, VectorEventType.ADDFEATURE, this.handleSourceFeatureAdded_, this),
listen(source, VectorEventType.CHANGEFEATURE, this.handleSourceFeatureChanged_, this),
listen(source, VectorEventType.REMOVEFEATURE, this.handleSourceFeatureDelete_, this)
listen(source, VectorEventType.REMOVEFEATURE, this.handleSourceFeatureDelete_, this),
listen(source, VectorEventType.CLEAR, this.handleSourceFeatureClear_, this)
];
source.forEachFeature(function(feature) {
this.featureCache_[getUid(feature)] = {
@@ -339,6 +340,14 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
this.featureCount_--;
}
/**
* @private
*/
handleSourceFeatureClear_() {
this.featureCache_ = {};
this.featureCount_ = 0;
}
/**
* @inheritDoc
*/

View File

@@ -47,7 +47,7 @@ class ReprojImage extends ImageBase {
const triangulation = new Triangulation(
sourceProj, targetProj, limitedTargetExtent, maxSourceExtent,
sourceResolution * errorThresholdInPixels);
sourceResolution * errorThresholdInPixels, targetResolution);
const sourceExtent = triangulation.calculateSourceExtent();
const sourceImage = getImageFunction(sourceExtent, sourceResolution, pixelRatio);

View File

@@ -159,7 +159,7 @@ class ReprojTile extends Tile {
*/
this.triangulation_ = new Triangulation(
sourceProj, targetProj, limitedTargetExtent, maxSourceExtent,
sourceResolution * errorThresholdInPixels);
sourceResolution * errorThresholdInPixels, targetResolution);
if (this.triangulation_.getTriangles().length === 0) {
// no valid triangles -> EMPTY

View File

@@ -1,7 +1,7 @@
/**
* @module ol/reproj/Triangulation
*/
import {boundingExtent, createEmpty, extendCoordinate, getBottomLeft, getBottomRight,
import {boundingExtent, createEmpty, extendCoordinate, getArea, getBottomLeft, getBottomRight,
getTopLeft, getTopRight, getWidth, intersects} from '../extent.js';
import {modulo} from '../math.js';
import {getTransform} from '../proj.js';
@@ -49,8 +49,9 @@ class Triangulation {
* @param {import("../extent.js").Extent} targetExtent Target extent to triangulate.
* @param {import("../extent.js").Extent} maxSourceExtent Maximal source extent that can be used.
* @param {number} errorThreshold Acceptable error (in source units).
* @param {?number} opt_destinationResolution The (optional) resolution of the destination.
*/
constructor(sourceProj, targetProj, targetExtent, maxSourceExtent, errorThreshold) {
constructor(sourceProj, targetProj, targetExtent, maxSourceExtent, errorThreshold, opt_destinationResolution) {
/**
* @type {import("../proj/Projection.js").default}
@@ -138,11 +139,26 @@ class Triangulation {
const sourceBottomRight = this.transformInv_(destinationBottomRight);
const sourceBottomLeft = this.transformInv_(destinationBottomLeft);
/*
* The maxSubdivision controls how many splittings of the target area can
* be done. The idea here is to do a linear mapping of the target areas
* but the actual overal reprojection (can be) extremely non-linear. The
* default value of MAX_SUBDIVISION was chosen based on mapping a 256x256
* tile size. However this function is also called to remap canvas rendered
* layers which can be much larger. This calculation increases the maxSubdivision
* value by the right factor so that each 256x256 pixel area has
* MAX_SUBDIVISION divisions.
*/
const maxSubdivision = MAX_SUBDIVISION + (opt_destinationResolution ?
Math.max(0, Math.ceil(Math.log2(getArea(targetExtent) /
(opt_destinationResolution * opt_destinationResolution * 256 * 256))))
: 0);
this.addQuad_(
destinationTopLeft, destinationTopRight,
destinationBottomRight, destinationBottomLeft,
sourceTopLeft, sourceTopRight, sourceBottomRight, sourceBottomLeft,
MAX_SUBDIVISION);
maxSubdivision);
if (this.wrapsXInSource_) {
let leftBound = Infinity;

View File

@@ -11,16 +11,21 @@ import {clamp} from './math.js';
*/
/**
* Returns a modified resolution taking into acocunt the viewport size and maximum
* Returns a modified resolution taking into account the viewport size and maximum
* allowed extent.
* @param {number} resolution Resolution
* @param {import("./extent.js").Extent=} maxExtent Maximum allowed extent.
* @param {import("./size.js").Size} viewportSize Viewport size.
* @param {boolean} showFullExtent Whether to show the full extent.
* @return {number} Capped resolution.
*/
function getViewportClampedResolution(resolution, maxExtent, viewportSize) {
function getViewportClampedResolution(resolution, maxExtent, viewportSize, showFullExtent) {
const xResolution = getWidth(maxExtent) / viewportSize[0];
const yResolution = getHeight(maxExtent) / viewportSize[1];
if (showFullExtent) {
return Math.min(resolution, Math.max(xResolution, yResolution));
}
return Math.min(resolution, Math.min(xResolution, yResolution));
}
@@ -52,9 +57,10 @@ function getSmoothClampedResolution(resolution, maxResolution, minResolution) {
* @param {Array<number>} resolutions Resolutions.
* @param {boolean=} opt_smooth If true, the view will be able to slightly exceed resolution limits. Default: true.
* @param {import("./extent.js").Extent=} opt_maxExtent Maximum allowed extent.
* @param {boolean=} opt_showFullExtent If true, allows us to show the full extent. Default: false.
* @return {Type} Zoom function.
*/
export function createSnapToResolutions(resolutions, opt_smooth, opt_maxExtent) {
export function createSnapToResolutions(resolutions, opt_smooth, opt_maxExtent, opt_showFullExtent) {
return (
/**
* @param {number|undefined} resolution Resolution.
@@ -68,7 +74,7 @@ export function createSnapToResolutions(resolutions, opt_smooth, opt_maxExtent)
const maxResolution = resolutions[0];
const minResolution = resolutions[resolutions.length - 1];
const cappedMaxRes = opt_maxExtent ?
getViewportClampedResolution(maxResolution, opt_maxExtent, size) :
getViewportClampedResolution(maxResolution, opt_maxExtent, size, opt_showFullExtent) :
maxResolution;
// during interacting or animating, allow intermediary values
@@ -100,9 +106,10 @@ export function createSnapToResolutions(resolutions, opt_smooth, opt_maxExtent)
* @param {number=} opt_minResolution Minimum resolution.
* @param {boolean=} opt_smooth If true, the view will be able to slightly exceed resolution limits. Default: true.
* @param {import("./extent.js").Extent=} opt_maxExtent Maximum allowed extent.
* @param {boolean=} opt_showFullExtent If true, allows us to show the full extent. Default: false.
* @return {Type} Zoom function.
*/
export function createSnapToPower(power, maxResolution, opt_minResolution, opt_smooth, opt_maxExtent) {
export function createSnapToPower(power, maxResolution, opt_minResolution, opt_smooth, opt_maxExtent, opt_showFullExtent) {
return (
/**
* @param {number|undefined} resolution Resolution.
@@ -114,7 +121,7 @@ export function createSnapToPower(power, maxResolution, opt_minResolution, opt_s
function(resolution, direction, size, opt_isMoving) {
if (resolution !== undefined) {
const cappedMaxRes = opt_maxExtent ?
getViewportClampedResolution(maxResolution, opt_maxExtent, size) :
getViewportClampedResolution(maxResolution, opt_maxExtent, size, opt_showFullExtent) :
maxResolution;
const minResolution = opt_minResolution !== undefined ? opt_minResolution : 0;
@@ -148,9 +155,10 @@ export function createSnapToPower(power, maxResolution, opt_minResolution, opt_s
* @param {number} minResolution Min resolution.
* @param {boolean=} opt_smooth If true, the view will be able to slightly exceed resolution limits. Default: true.
* @param {import("./extent.js").Extent=} opt_maxExtent Maximum allowed extent.
* @param {boolean=} opt_showFullExtent If true, allows us to show the full extent. Default: false.
* @return {Type} Zoom function.
*/
export function createMinMaxResolution(maxResolution, minResolution, opt_smooth, opt_maxExtent) {
export function createMinMaxResolution(maxResolution, minResolution, opt_smooth, opt_maxExtent, opt_showFullExtent) {
return (
/**
* @param {number|undefined} resolution Resolution.
@@ -162,7 +170,7 @@ export function createMinMaxResolution(maxResolution, minResolution, opt_smooth,
function(resolution, direction, size, opt_isMoving) {
if (resolution !== undefined) {
const cappedMaxRes = opt_maxExtent ?
getViewportClampedResolution(maxResolution, opt_maxExtent, size) :
getViewportClampedResolution(maxResolution, opt_maxExtent, size, opt_showFullExtent) :
maxResolution;
const smooth = opt_smooth !== undefined ? opt_smooth : true;

View File

@@ -71,7 +71,7 @@ export function createSnapToZero(opt_tolerance) {
return (
/**
* @param {number|undefined} rotation Rotation.
* @param {boolean} opt_isMoving True if an interaction or animation is in progress.
* @param {boolean=} opt_isMoving True if an interaction or animation is in progress.
* @return {number|undefined} Rotation.
*/
function(rotation, opt_isMoving) {

View File

@@ -50,7 +50,7 @@ const TOS_ATTRIBUTION = '<a class="ol-attribution-bing-tos" ' +
/**
* @typedef {Object} Options
* @property {number} [cacheSize] Tile cache size. The default depends on the screen size. Will increase if too small.
* @property {number} [cacheSize] Tile cache size. The default depends on the screen size. Will be ignored if too small.
* @property {boolean} [hidpi=false] If `true` hidpi tiles will be requested.
* @property {string} [culture='en-us'] Culture code.
* @property {string} key Bing Maps API key. Get yours at http://www.bingmapsportal.com/.

View File

@@ -9,7 +9,7 @@ import XYZ from './XYZ.js';
/**
* @typedef {Object} Options
* @property {import("./Source.js").AttributionLike} [attributions] Attributions.
* @property {number} [cacheSize] Tile cache size. The default depends on the screen size. Will increase if too small.
* @property {number} [cacheSize] Tile cache size. The default depends on the screen size. Will be ignored if too small.
* @property {null|string} [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.
* See https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image for more detail.

View File

@@ -29,7 +29,7 @@ import VectorSource from './Vector.js';
* ```
* See {@link module:ol/geom/Polygon~Polygon#getInteriorPoint} for a way to get a cluster
* calculation point for polygons.
* @property {VectorSource} source Source.
* @property {VectorSource} [source] Source.
* @property {boolean} [wrapX=true] Whether to wrap the world horizontally.
*/
@@ -39,6 +39,10 @@ import VectorSource from './Vector.js';
* Layer source to cluster vector data. Works out of the box with point
* geometries. For other geometry types, or if not all geometries should be
* considered for clustering, a custom `geometryFunction` can be defined.
*
* If the instance is disposed without also disposing the underlying
* source `setSource(null)` has to be called to remove the listener reference
* from the wrapped source.
* @api
*/
class Cluster extends VectorSource {
@@ -81,13 +85,17 @@ class Cluster extends VectorSource {
return geometry;
};
/**
* @type {VectorSource}
* @protected
*/
this.source = options.source;
this.boundRefresh_ = this.refresh.bind(this);
this.source.addEventListener(EventType.CHANGE, this.refresh.bind(this));
this.setSource(options.source || null);
}
/**
* @override
*/
clear(opt_fast) {
this.features.length = 0;
super.clear(opt_fast);
}
/**
@@ -132,7 +140,23 @@ class Cluster extends VectorSource {
}
/**
* handle the source changing
* Replace the wrapped source.
* @param {VectorSource} source The new source for this instance.
* @api
*/
setSource(source) {
if (this.source) {
this.source.removeEventListener(EventType.CHANGE, this.boundRefresh_);
}
this.source = source;
if (source) {
source.addEventListener(EventType.CHANGE, this.boundRefresh_);
}
this.refresh();
}
/**
* Handle the source changing.
* @override
*/
refresh() {
@@ -145,10 +169,9 @@ class Cluster extends VectorSource {
* @protected
*/
cluster() {
if (this.resolution === undefined) {
if (this.resolution === undefined || !this.source) {
return;
}
this.features.length = 0;
const extent = createEmpty();
const mapDistance = this.distance * this.resolution;
const features = this.source.getFeatures();

View File

@@ -9,6 +9,7 @@ import {Versions} from '../format/IIIFInfo.js';
import {assert} from '../asserts.js';
import TileGrid from '../tilegrid/TileGrid.js';
import TileImage from './TileImage.js';
import {toSize} from '../size.js';
/**
* @typedef {Object} Options
@@ -58,7 +59,7 @@ function formatPercentage(percentage) {
class IIIF extends TileImage {
/**
* @param {Options} opt_options Tile source options. Use {@link import("../format/IIIFInfo.js").IIIFInfo}
* @param {Options=} opt_options Tile source options. Use {@link import("../format/IIIFInfo.js").IIIFInfo}
* to parse Image API service information responses into constructor options.
* @api
*/
@@ -87,7 +88,7 @@ class IIIF extends TileImage {
const extent = options.extent || [0, -height, width, 0];
const supportsListedSizes = sizes != undefined && Array.isArray(sizes) && sizes.length > 0;
const supportsListedTiles = tileSize != undefined && (typeof tileSize === 'number' && Number.isInteger(tileSize) && tileSize > 0 || Array.isArray(tileSize) && tileSize.length > 0);
const supportsListedTiles = tileSize !== undefined && (typeof tileSize === 'number' && Number.isInteger(tileSize) && tileSize > 0 || Array.isArray(tileSize) && tileSize.length > 0);
const supportsArbitraryTiling = supports != undefined && Array.isArray(supports) &&
(supports.includes('regionByPx') || supports.includes('regionByPct')) &&
(supports.includes('sizeByWh') || supports.includes('sizeByH') ||
@@ -265,7 +266,9 @@ class IIIF extends TileImage {
return baseUrl + regionParam + '/' + sizeParam + '/0/' + quality + '.' + format;
};
const IiifTileClass = CustomTile.bind(null, tilePixelRatio, tileGrid);
const IiifTileClass = CustomTile.bind(null, toSize(tileSize || 256).map(function(size) {
return size * tilePixelRatio;
}));
super({
attributions: options.attributions,

View File

@@ -15,7 +15,7 @@ import Source from './Source.js';
/**
* @enum {string}
*/
const ImageSourceEventType = {
export const ImageSourceEventType = {
/**
* Triggered when an image starts loading.

View File

@@ -33,7 +33,7 @@ import ImageSource from './Image.js';
* the value returned by the function is later changed then
* `changed` should be called on the source for the source to
* invalidate the current cached image. See: {@link module:ol/Observable~Observable#changed}
* @property {import("../proj.js").ProjectionLike} projection Projection.
* @property {import("../proj.js").ProjectionLike} [projection] Projection. Default is the view projection.
* @property {number} [ratio=1.5] Ratio. 1 means canvases are the size of the map viewport, 2 means twice the
* width and height of the map viewport, and so on. Must be `1` or higher.
* @property {Array<number>} [resolutions] Resolutions.

View File

@@ -20,7 +20,7 @@ import {appendParams} from '../uri.js';
* @property {boolean} [hidpi=true] Use the `ol/Map#pixelRatio` value when requesting
* the image from the remote server.
* @property {boolean} [useOverlay] If `true`, will use `GETDYNAMICMAPOVERLAYIMAGE`.
* @property {import("../proj.js").ProjectionLike} projection Projection.
* @property {import("../proj.js").ProjectionLike} [projection] Projection. Default is the view projection.
* @property {number} [ratio=1] Ratio. `1` means image requests are the size of the map viewport, `2` means
* twice the width and height of the map viewport, and so on. Must be `1` or higher.
* @property {Array<number>} [resolutions] Resolutions.

View File

@@ -19,7 +19,7 @@ import ImageSource, {defaultImageLoadFunction} from './Image.js';
* @property {import("../extent.js").Extent} [imageExtent] Extent of the image in map coordinates.
* This is the [left, bottom, right, top] map coordinates of your image.
* @property {import("../Image.js").LoadFunction} [imageLoadFunction] Optional function to load an image given a URL.
* @property {import("../proj.js").ProjectionLike} projection Projection.
* @property {import("../proj.js").ProjectionLike} [projection] Projection. Default is the view projection.
* @property {import("../size.js").Size} [imageSize] Size of the image in pixels. Usually the image size is auto-detected, so this
* only needs to be set if auto-detection fails for some reason.
* @property {string} url Image URL.

View File

@@ -20,7 +20,7 @@ export const ATTRIBUTION = '&#169; ' +
/**
* @typedef {Object} Options
* @property {import("./Source.js").AttributionLike} [attributions] Attributions.
* @property {number} [cacheSize] Tile cache size. The default depends on the screen size. Will increase if too small.
* @property {number} [cacheSize] Tile cache size. The default depends on the screen size. Will be ignored if too small.
* @property {null|string} [crossOrigin='anonymous'] 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.
* See https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image for more detail.

View File

@@ -90,7 +90,7 @@ const ProviderConfig = {
/**
* @typedef {Object} Options
* @property {number} [cacheSize] Tile cache size. The default depends on the screen size. Will increase if too small.
* @property {number} [cacheSize] Tile cache size. The default depends on the screen size. Will be ignored if too small.
* @property {string} layer Layer name.
* @property {number} [minZoom] Minimum zoom.
* @property {number} [maxZoom] Maximum zoom.

View File

@@ -69,24 +69,21 @@ class TileSource extends Source {
*/
this.tileGrid = options.tileGrid !== undefined ? options.tileGrid : null;
let cacheSize = options.cacheSize;
if (cacheSize === undefined) {
const tileSize = [256, 256];
const tileGrid = options.tileGrid;
if (tileGrid) {
toSize(tileGrid.getTileSize(tileGrid.getMinZoom()), tileSize);
}
const canUseScreen = typeof screen !== 'undefined';
const width = canUseScreen ? (screen.availWidth || screen.width) : 1920;
const height = canUseScreen ? (screen.availHeight || screen.height) : 1080;
cacheSize = 4 * Math.ceil(width / tileSize[0]) * Math.ceil(height / tileSize[1]);
const tileSize = [256, 256];
const tileGrid = options.tileGrid;
if (tileGrid) {
toSize(tileGrid.getTileSize(tileGrid.getMinZoom()), tileSize);
}
const canUseScreen = typeof screen !== 'undefined';
const width = canUseScreen ? (screen.availWidth || screen.width) : 1920;
const height = canUseScreen ? (screen.availHeight || screen.height) : 1080;
const minCacheSize = 4 * Math.ceil(width / tileSize[0]) * Math.ceil(height / tileSize[1]);
/**
* @protected
* @type {import("../TileCache.js").default}
*/
this.tileCache = new TileCache(cacheSize);
this.tileCache = new TileCache(Math.max(minCacheSize, options.cacheSize || 0));
/**
* @protected
@@ -125,7 +122,7 @@ class TileSource extends Source {
/**
* @param {import("../proj/Projection.js").default} projection Projection.
* @param {!Object<string, import("../TileRange.js").default>} usedTiles Used tiles.
* @param {!Object<string, boolean>} usedTiles Used tiles.
*/
expireCache(projection, usedTiles) {
const tileCache = this.getTileCacheForProjection(projection);
@@ -317,9 +314,6 @@ class TileSource extends Source {
this.tileCache.clear();
}
/**
* @inheritDoc
*/
refresh() {
this.clear();
super.refresh();

View File

@@ -13,7 +13,7 @@ import {appendParams} from '../uri.js';
/**
* @typedef {Object} Options
* @property {import("./Source.js").AttributionLike} [attributions] Attributions.
* @property {number} [cacheSize] Tile cache size. The default depends on the screen size. Will increase if too small.
* @property {number} [cacheSize] Tile cache size. The default depends on the screen size. Will be ignored if too small.
* @property {null|string} [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.
* See https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image for more detail.

View File

@@ -17,7 +17,7 @@ import {getForProjection as getTileGridForProjection} from '../tilegrid.js';
* @typedef {Object} Options
* @property {import("./Source.js").AttributionLike} [attributions] Attributions.
* @property {boolean} [attributionsCollapsible=true] Attributions are collapsible.
* @property {number} [cacheSize] Tile cache size. The default depends on the screen size. Will increase if too small.
* @property {number} [cacheSize] Tile cache size. The default depends on the screen size. Will be ignored if too small.
* @property {null|string} [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.
* See https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image for more detail.

View File

@@ -39,7 +39,7 @@ import {createXYZ, extentFromProjection} from '../tilegrid.js';
/**
* @typedef {Object} Options
* @property {import("./Source.js").AttributionLike} [attributions] Attributions.
* @property {number} [cacheSize] Tile cache size. The default depends on the screen size. Will increase if too small.
* @property {number} [cacheSize] Tile cache size. The default depends on the screen size. Will be ignored if too small.
* @property {null|string} [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.
* See https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image for more detail.

View File

@@ -20,7 +20,7 @@ import {appendParams} from '../uri.js';
/**
* @typedef {Object} Options
* @property {import("./Source.js").AttributionLike} [attributions] Attributions.
* @property {number} [cacheSize] Tile cache size. The default depends on the screen size. Will increase if too small.
* @property {number} [cacheSize] Tile cache size. The default depends on the screen size. Will be ignored if too small.
* @property {null|string} [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.
* See https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image for more detail.

View File

@@ -190,7 +190,7 @@ class VectorSource extends Source {
* @private
* @type {boolean}
*/
this.overlaps_ = options.overlaps == undefined ? true : options.overlaps;
this.overlaps_ = options.overlaps === undefined ? true : options.overlaps;
/**
* @private
@@ -914,9 +914,6 @@ class VectorSource extends Source {
}
}
/**
* @inheritDoc
*/
refresh() {
this.clear(true);
this.loadedExtentsRtree_.clear();

View File

@@ -30,9 +30,10 @@ import TileCache from '../TileCache.js';
* @property {import("./State.js").default} [state] Source state.
* @property {typeof import("../VectorTile.js").default} [tileClass] Class used to instantiate image tiles.
* Default is {@link module:ol/VectorTile}.
* @property {number} [maxZoom=22] Optional max zoom level.
* @property {number} [minZoom] Optional min zoom level.
* @property {number|import("../size.js").Size} [tileSize=512] Optional tile size.
* @property {number} [maxZoom=22] Optional max zoom level. Not used if `tileGrid` is provided.
* @property {number} [minZoom] Optional min zoom level. Not used if `tileGrid` is provided.
* @property {number|import("../size.js").Size} [tileSize=512] Optional tile size. Not used if `tileGrid` is provided.
* @property {number} [maxResolution] Optional tile grid resolution at level zero. Not used if `tileGrid` is provided.
* @property {import("../tilegrid/TileGrid.js").default} [tileGrid] Tile grid.
* @property {import("../Tile.js").LoadFunction} [tileLoadFunction]
* Optional function to load a tile given a URL. Could look like this for pbf tiles:
@@ -105,6 +106,7 @@ class VectorTile extends UrlTile {
const tileGrid = options.tileGrid || createXYZ({
extent: extent,
maxResolution: options.maxResolution,
maxZoom: options.maxZoom !== undefined ? options.maxZoom : 22,
minZoom: options.minZoom,
tileSize: options.tileSize || 512
@@ -225,6 +227,15 @@ class VectorTile extends UrlTile {
this.sourceTileCache.clear();
}
/**
* @param {import("../proj/Projection.js").default} projection Projection.
* @param {!Object<string, boolean>} usedTiles Used tiles.
*/
expireCache(projection, usedTiles) {
super.expireCache(projection, usedTiles);
this.sourceTileCache.expireCache({});
}
/**
* @param {number} pixelRatio Pixel ratio.
* @param {import("../proj/Projection").default} projection Projection.
@@ -326,7 +337,6 @@ class VectorTile extends UrlTile {
tile.sourceTiles = sourceTiles;
}
}
this.sourceTileCache.expireCache({});
return sourceTiles;
}

View File

@@ -4,9 +4,8 @@
import {expandUrl, createFromTileUrlFunctions, nullTileUrlFunction} from '../tileurlfunction.js';
import {find, findIndex, includes} from '../array.js';
import {containsExtent} from '../extent.js';
import {assign} from '../obj.js';
import {get as getProjection, equivalent, transformExtent} from '../proj.js';
import {get as getProjection, equivalent} from '../proj.js';
import TileImage from './TileImage.js';
import WMTSRequestEncoding from './WMTSRequestEncoding.js';
import {createFromCapabilitiesMatrixSet} from '../tilegrid/WMTS.js';
@@ -15,7 +14,7 @@ import {appendParams} from '../uri.js';
/**
* @typedef {Object} Options
* @property {import("./Source.js").AttributionLike} [attributions] Attributions.
* @property {number} [cacheSize] Tile cache size. The default depends on the screen size. Will increase if too small.
* @property {number} [cacheSize] Tile cache size. The default depends on the screen size. Will be ignored if too small.
* @property {null|string} [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.
* See https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image for more detail.
@@ -377,22 +376,25 @@ export function optionsFromCapabilities(wmtsCap, config) {
}
}
const wgs84BoundingBox = l['WGS84BoundingBox'];
let extent, wrapX;
if (wgs84BoundingBox !== undefined) {
const wgs84ProjectionExtent = getProjection('EPSG:4326').getExtent();
wrapX = (wgs84BoundingBox[0] == wgs84ProjectionExtent[0] &&
wgs84BoundingBox[2] == wgs84ProjectionExtent[2]);
extent = transformExtent(
wgs84BoundingBox, 'EPSG:4326', projection);
const projectionExtent = projection.getExtent();
if (projectionExtent) {
// If possible, do a sanity check on the extent - it should never be
// bigger than the validity extent of the projection of a matrix set.
if (!containsExtent(projectionExtent, extent)) {
extent = undefined;
}
}
const wrapX = false;
const matrix0 = matrixSetObj.TileMatrix[0];
const resolution = matrix0.ScaleDenominator * 0.00028; // WMTS 1.0.0: standardized rendering pixel size
const origin = projection === getProjection('EPSG:4326')
? [matrix0.TopLeftCorner[1], matrix0.TopLeftCorner[0]]
: matrix0.TopLeftCorner;
const tileSpanX = matrix0.TileWidth * resolution;
const tileSpanY = matrix0.TileHeight * resolution;
const extent = [
origin[0],
origin[1] - tileSpanY * matrix0.MatrixHeight,
origin[0] + tileSpanX * matrix0.MatrixWidth,
origin[1]
];
if (projection.getExtent() === null) {
projection.setExtent(extent);
}
const tileGrid = createFromCapabilitiesMatrixSet(matrixSetObj, extent, matrixLimits);

View File

@@ -9,7 +9,7 @@ import {createXYZ, extentFromProjection} from '../tilegrid.js';
* @typedef {Object} Options
* @property {import("./Source.js").AttributionLike} [attributions] Attributions.
* @property {boolean} [attributionsCollapsible=true] Attributions are collapsible.
* @property {number} [cacheSize] Tile cache size. The default depends on the screen size. Will increase if too small.
* @property {number} [cacheSize] Tile cache size. The default depends on the screen size. Will be ignored if too small.
* @property {null|string} [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.
* See https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image for more detail.
@@ -17,8 +17,9 @@ import {createXYZ, extentFromProjection} from '../tilegrid.js';
* @property {import("../proj.js").ProjectionLike} [projection='EPSG:3857'] Projection.
* @property {number} [reprojectionErrorThreshold=0.5] Maximum allowed reprojection error (in pixels).
* Higher values can increase reprojection performance, but decrease precision.
* @property {number} [maxZoom=18] Optional max zoom level.
* @property {number} [minZoom=0] Optional min zoom level.
* @property {number} [maxZoom=42] Optional max zoom level. Not used if `tileGrid` is provided.
* @property {number} [minZoom=0] Optional min zoom level. Not used if `tileGrid` is provided.
* @property {number} [maxResolution] Optional tile grid resolution at level zero. Not used if `tileGrid` is provided.
* @property {import("../tilegrid/TileGrid.js").default} [tileGrid] Tile grid.
* @property {import("../Tile.js").LoadFunction} [tileLoadFunction] Optional function to load a tile given a URL. The default is
* ```js
@@ -31,9 +32,10 @@ import {createXYZ, extentFromProjection} from '../tilegrid.js';
* by 512px images (for retina/hidpi devices) then `tilePixelRatio`
* should be set to `2`.
* @property {number|import("../size.js").Size} [tileSize=[256, 256]] The tile size used by the tile service.
* Not used if `tileGrid` is provided.
* @property {import("../Tile.js").UrlFunction} [tileUrlFunction] Optional function to get
* tile URL given a tile coordinate and the projection.
* Required if url or urls are not provided.
* Required if `url` or `urls` are not provided.
* @property {string} [url] URL template. Must include `{x}`, `{y}` or `{-y}`,
* and `{z}` placeholders. A `{?-?}` template pattern, for example `subdomain{a-f}.domain.com`,
* may be used instead of defining each one separately in the `urls` option.
@@ -78,6 +80,7 @@ class XYZ extends TileImage {
const tileGrid = options.tileGrid !== undefined ? options.tileGrid :
createXYZ({
extent: extentFromProjection(projection),
maxResolution: options.maxResolution,
maxZoom: options.maxZoom,
minZoom: options.minZoom,
tileSize: options.tileSize

View File

@@ -8,10 +8,10 @@ import TileState from '../TileState.js';
import {expandUrl, createFromTileUrlFunctions} from '../tileurlfunction.js';
import {assert} from '../asserts.js';
import {createCanvasContext2D} from '../dom.js';
import {getTopLeft} from '../extent.js';
import {toSize} from '../size.js';
import TileImage from './TileImage.js';
import TileGrid from '../tilegrid/TileGrid.js';
import {getCenter} from '../extent.js';
/**
@@ -26,8 +26,7 @@ const TierSizeCalculation = {
export class CustomTile extends ImageTile {
/**
* @param {number} tilePixelRatio Tile pixel ratio to display the tile
* @param {import("../tilegrid/TileGrid.js").default} tileGrid TileGrid that the tile belongs to.
* @param {import("../size.js").Size} tileSize Full tile size.
* @param {import("../tilecoord.js").TileCoord} tileCoord Tile coordinate.
* @param {TileState} state State.
* @param {string} src Image source URI.
@@ -35,7 +34,8 @@ export class CustomTile extends ImageTile {
* @param {import("../Tile.js").LoadFunction} tileLoadFunction Tile load function.
* @param {import("../Tile.js").Options=} opt_options Tile options.
*/
constructor(tilePixelRatio, tileGrid, tileCoord, state, src, crossOrigin, tileLoadFunction, opt_options) {
constructor(tileSize, tileCoord, state, src, crossOrigin, tileLoadFunction, opt_options) {
super(tileCoord, state, src, crossOrigin, tileLoadFunction, opt_options);
/**
@@ -45,14 +45,10 @@ export class CustomTile extends ImageTile {
this.zoomifyImage_ = null;
/**
* @private
* @type {import("../size.js").Size}
*/
this.tileSize_ = toSize(tileGrid.getTileSize(tileCoord[0])).map(
function(x) {
return x * tilePixelRatio;
}
);
this.tileSize_ = tileSize;
}
/**
@@ -85,7 +81,7 @@ export class CustomTile extends ImageTile {
/**
* @typedef {Object} Options
* @property {import("./Source.js").AttributionLike} [attributions] Attributions.
* @property {number} [cacheSize] Tile cache size. The default depends on the screen size. Will increase if too small.
* @property {number} [cacheSize] Tile cache size. The default depends on the screen size. Will be ignored if too small.
* @property {null|string} [crossOrigin] The `crossOrigin` attribute for loaded images. Note that
* you must provide a `crossOrigin` value you want to access pixel data with the Canvas renderer.
* See https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image for more detail.
@@ -93,7 +89,7 @@ export class CustomTile extends ImageTile {
* @property {number} [tilePixelRatio] The pixel ratio used by the tile service. For example, if the tile service advertizes 256px by 256px tiles but actually sends 512px by 512px images (for retina/hidpi devices) then `tilePixelRatio` should be set to `2`
* @property {number} [reprojectionErrorThreshold=0.5] Maximum allowed reprojection error (in pixels).
* Higher values can increase reprojection performance, but decrease precision.
* @property {string} [url] URL template or base URL of the Zoomify service.
* @property {string} url URL template or base URL of the Zoomify service.
* A base URL is the fixed part
* of the URL, excluding the tile group, z, x, and y folder structure, e.g.
* `http://my.zoomify.info/IMAGE.TIF/`. A URL template must include
@@ -105,7 +101,7 @@ export class CustomTile extends ImageTile {
* A `{?-?}` template pattern, for example `subdomain{a-f}.domain.com`, may be
* used instead of defining each one separately in the `urls` option.
* @property {string} [tierSizeCalculation] Tier size calculation method: `default` or `truncated`.
* @property {import("../size.js").Size} [size] Size of the image.
* @property {import("../size.js").Size} size
* @property {import("../extent.js").Extent} [extent] Extent for the TileGrid that is created.
* Default sets the TileGrid in the
* fourth quadrant, meaning extent is `[0, -height, width, 0]`. To change the
@@ -114,8 +110,8 @@ export class CustomTile extends ImageTile {
* @property {number} [transition] Duration of the opacity transition for rendering.
* To disable the opacity transition, pass `transition: 0`.
* @property {number} [tileSize=256] Tile size. Same tile size is used for all zoom levels.
* @property {number} [zDirection=0] Indicate which resolution should be used
* by a renderer if the view resolution does not match any resolution of the tile source.
* @property {number} [zDirection] Indicate which resolution should be used
* by a renderer if the views resolution does not match any resolution of the tile source.
* If 0, the nearest resolution will be used. If 1, the nearest lower resolution
* will be used. If -1, the nearest higher resolution will be used.
*/
@@ -130,24 +126,23 @@ export class CustomTile extends ImageTile {
class Zoomify extends TileImage {
/**
* @param {Options=} opt_options Options.
* @param {Options} opt_options Options.
*/
constructor(opt_options) {
const options = opt_options || {};
const options = opt_options;
const size = options.size;
const tierSizeCalculation = options.tierSizeCalculation !== undefined ?
options.tierSizeCalculation :
TierSizeCalculation.DEFAULT;
const tilePixelRatio = options.tilePixelRatio || 1;
const imageWidth = size[0];
const imageHeight = size[1];
const extent = options.extent || [0, -size[1], size[0], 0];
const tierSizeInTiles = [];
const tileSize = options.tileSize || DEFAULT_TILE_SIZE;
const tilePixelRatio = options.tilePixelRatio || 1;
let tileSizeForTierSizeCalculation = tileSize;
let tileSizeForTierSizeCalculation = tileSize * tilePixelRatio;
switch (tierSizeCalculation) {
case TierSizeCalculation.DEFAULT:
@@ -179,10 +174,10 @@ class Zoomify extends TileImage {
tierSizeInTiles.push([1, 1]);
tierSizeInTiles.reverse();
const resolutions = [1];
const resolutions = [tilePixelRatio];
const tileCountUpToTier = [0];
for (let i = 1, ii = tierSizeInTiles.length; i < ii; i++) {
resolutions.push(1 << i);
resolutions.push(tilePixelRatio << i);
tileCountUpToTier.push(
tierSizeInTiles[i - 1][0] * tierSizeInTiles[i - 1][1] +
tileCountUpToTier[i - 1]
@@ -192,8 +187,7 @@ class Zoomify extends TileImage {
const tileGrid = new TileGrid({
tileSize: tileSize,
extent: extent,
origin: getTopLeft(extent),
extent: options.extent || [0, -imageHeight, imageWidth, 0],
resolutions: resolutions
});
@@ -203,6 +197,8 @@ class Zoomify extends TileImage {
}
const urls = expandUrl(url);
let tileWidth = tileSize * tilePixelRatio;
/**
* @param {string} template Template.
* @return {import("../Tile.js").UrlFunction} Tile URL function.
@@ -226,8 +222,6 @@ class Zoomify extends TileImage {
const tileIndex =
tileCoordX +
tileCoordY * tierSizeInTiles[tileCoordZ][0];
const tileSize = tileGrid.getTileSize(tileCoordZ);
const tileWidth = Array.isArray(tileSize) ? tileSize[0] : tileSize;
const tileGroup = ((tileIndex + tileCountUpToTier[tileCoordZ]) / tileWidth) | 0;
const localContext = {
'z': tileCoordZ,
@@ -246,7 +240,7 @@ class Zoomify extends TileImage {
const tileUrlFunction = createFromTileUrlFunctions(urls.map(createFromTemplate));
const ZoomifyTileClass = CustomTile.bind(null, tilePixelRatio, tileGrid);
const ZoomifyTileClass = CustomTile.bind(null, toSize(tileSize * tilePixelRatio));
super({
attributions: options.attributions,
@@ -266,6 +260,19 @@ class Zoomify extends TileImage {
*/
this.zDirection = options.zDirection;
// Server retina tile detection (non-standard):
// Try loading the center tile for the highest resolution. If it is not
// available, we are dealing with retina tiles, and need to adjust the
// tile url calculation.
const tileUrl = tileGrid.getTileCoordForCoordAndResolution(getCenter(tileGrid.getExtent()), resolutions[resolutions.length - 1]);
const testTileUrl = tileUrlFunction(tileUrl, 1, null);
const image = new Image();
image.addEventListener('error', function() {
tileWidth = tileSize;
this.changed();
}.bind(this));
image.src = testTileUrl;
}
}

View File

@@ -1,10 +1,3 @@
/**
* @license
* Latitude/longitude spherical geodesy formulae taken from
* http://www.movable-type.co.uk/scripts/latlong.html
* Licensed under CC-BY-3.0.
*/
/**
* @module ol/sphere
*/

View File

@@ -3,8 +3,6 @@
*/
import {assert} from '../asserts.js';
import EventTarget from '../events/Target.js';
import EventType from '../events/EventType.js';
/**
@@ -25,15 +23,13 @@ import EventType from '../events/EventType.js';
* @fires import("../events/Event.js").default
* @template T
*/
class LRUCache extends EventTarget {
class LRUCache {
/**
* @param {number=} opt_highWaterMark High water mark.
*/
constructor(opt_highWaterMark) {
super();
/**
* @type {number}
*/
@@ -82,7 +78,6 @@ class LRUCache extends EventTarget {
this.entries_ = {};
this.oldest_ = null;
this.newest_ = null;
this.dispatchEvent(EventType.CLEAR);
}

View File

@@ -10,6 +10,7 @@ import RegularShape from './RegularShape.js';
* @property {import("./Fill.js").default} [fill] Fill style.
* @property {number} radius Circle radius.
* @property {import("./Stroke.js").default} [stroke] Stroke style.
* @property {Array<number>} [displacement=[0,0]] displacement
*/
@@ -30,7 +31,8 @@ class CircleStyle extends RegularShape {
points: Infinity,
fill: options.fill,
radius: options.radius,
stroke: options.stroke
stroke: options.stroke,
displacement: options.displacement !== undefined ? options.displacement : [0, 0]
});
}
@@ -45,7 +47,8 @@ class CircleStyle extends RegularShape {
const style = new CircleStyle({
fill: this.getFill() ? this.getFill().clone() : undefined,
stroke: this.getStroke() ? this.getStroke().clone() : undefined,
radius: this.getRadius()
radius: this.getRadius(),
displacement: this.getDisplacement().slice()
});
style.setOpacity(this.getOpacity());
style.setScale(this.getScale());

View File

@@ -33,6 +33,7 @@ import ImageStyle from './Image.js';
* to provide the size of the image, with the `imgSize` option.
* @property {Array<number>} [offset=[0, 0]] Offset, which, together with the size and the offset origin, define the
* sub-rectangle to use from the original icon image.
* @property {Array<number>} [displacement=[0,0]] Displacement the icon
* @property {import("./IconOrigin.js").default} [offsetOrigin='top-left'] Origin of the offset: `bottom-left`, `bottom-right`,
* `top-left` or `top-right`.
* @property {number} [opacity=1] Opacity of the icon.
@@ -84,6 +85,7 @@ class Icon extends ImageStyle {
opacity: opacity,
rotation: rotation,
scale: scale,
displacement: options.displacement !== undefined ? options.displacement : [0, 0],
rotateWithView: rotateWithView
});
@@ -177,7 +179,6 @@ class Icon extends ImageStyle {
* @type {Array<number>}
*/
this.offset_ = options.offset !== undefined ? options.offset : [0, 0];
/**
* @private
* @type {import("./IconOrigin.js").default}
@@ -336,6 +337,7 @@ class Icon extends ImageStyle {
return this.origin_;
}
let offset = this.offset_;
const displacement = this.getDisplacement();
if (this.offsetOrigin_ != IconOrigin.TOP_LEFT) {
const size = this.getSize();
@@ -353,6 +355,8 @@ class Icon extends ImageStyle {
offset[1] = iconImageSize[1] - size[1] - offset[1];
}
}
offset[0] += displacement[0];
offset[1] += displacement[1];
this.origin_ = offset;
return this.origin_;
}

View File

@@ -7,6 +7,14 @@
* @enum {string}
*/
export default {
/**
* Anchor is a fraction
* @api
*/
FRACTION: 'fraction',
/**
* Anchor is in pixels
* @api
*/
PIXELS: 'pixels'
};

View File

@@ -85,15 +85,18 @@ class IconImage extends EventTarget {
/**
* @private
* @param {CanvasRenderingContext2D=} context A context with the image already drawn into.
* @return {boolean} The image canvas is tainted.
*/
isTainted_() {
isTainted_(context) {
if (this.tainted_ === undefined && this.imageState_ === ImageState.LOADED) {
this.tainted_ = false;
const context = createCanvasContext2D(1, 1);
try {
if (!context) {
context = createCanvasContext2D(1, 1);
context.drawImage(this.image_, 0, 0);
}
try {
context.getImageData(0, 0, 1, 1);
this.tainted_ = false;
} catch (e) {
this.tainted_ = true;
}
@@ -203,7 +206,7 @@ class IconImage extends EventTarget {
* @private
*/
replaceColor_() {
if (!this.color_ || this.isTainted_()) {
if (!this.color_) {
return;
}
@@ -213,6 +216,25 @@ class IconImage extends EventTarget {
const ctx = this.canvas_.getContext('2d');
ctx.drawImage(this.image_, 0, 0);
if (this.isTainted_(ctx)) {
// If reading from the canvas throws a SecurityError the same effect can be
// achieved with globalCompositeOperation.
// This could be used as the default, but it is not fully supported by all
// browsers. E. g. Internet Explorer 11 does not support the multiply
// operation and the resulting image shape will be completelly filled with
// the provided color.
// So this is only used as a fallback. It is still better than having no icon
// at all.
const c = this.color_;
ctx.globalCompositeOperation = 'multiply';
ctx.fillStyle = 'rgb(' + c[0] + ',' + c[1] + ',' + c[2] + ')';
ctx.fillRect(0, 0, this.image_.width, this.image_.height);
ctx.globalCompositeOperation = 'destination-in';
ctx.drawImage(this.image_, 0, 0);
return;
}
const imgData = ctx.getImageData(0, 0, this.image_.width, this.image_.height);
const data = imgData.data;
const r = this.color_[0] / 255.0;

View File

@@ -7,8 +7,24 @@
* @enum {string}
*/
export default {
/**
* Origin is at bottom left
* @api
*/
BOTTOM_LEFT: 'bottom-left',
/**
* Origin is at bottom right
* @api
*/
BOTTOM_RIGHT: 'bottom-right',
/**
* Origin is at top left
* @api
*/
TOP_LEFT: 'top-left',
/**
* Origin is at top right
* @api
*/
TOP_RIGHT: 'top-right'
};

View File

@@ -10,6 +10,7 @@ import {abstract} from '../util.js';
* @property {boolean} rotateWithView
* @property {number} rotation
* @property {number} scale
* @property {Array<number>} displacement
*/
@@ -51,6 +52,12 @@ class ImageStyle {
*/
this.scale_ = options.scale;
/**
* @private
* @type {Array<number>}
*/
this.displacement_ = options.displacement;
}
/**
@@ -63,7 +70,8 @@ class ImageStyle {
opacity: this.getOpacity(),
scale: this.getScale(),
rotation: this.getRotation(),
rotateWithView: this.getRotateWithView()
rotateWithView: this.getRotateWithView(),
displacement: this.getDisplacement().slice()
});
}
@@ -103,6 +111,15 @@ class ImageStyle {
return this.scale_;
}
/**
* Get the displacement of the shape
* @return {Array<number>} Shape's center displacement
* @api
*/
getDisplacement() {
return this.displacement_;
}
/**
* Get the anchor point in pixels. The anchor determines the center point for the
* symbolizer.

View File

@@ -20,6 +20,7 @@ import ImageStyle from './Image.js';
* @property {number} [radius1] Outer radius of a star.
* @property {number} [radius2] Inner radius of a star.
* @property {number} [angle=0] Shape's angle in radians. A value of 0 will have one of the shape's point facing up.
* @property {Array<number>} [displacement=[0,0]] Displacement of the shape
* @property {import("./Stroke.js").default} [stroke] Stroke style.
* @property {number} [rotation=0] Rotation in radians (positive rotation clockwise).
* @property {boolean} [rotateWithView=false] Whether to rotate the shape with the view.
@@ -61,7 +62,8 @@ class RegularShape extends ImageStyle {
opacity: 1,
rotateWithView: rotateWithView,
rotation: options.rotation !== undefined ? options.rotation : 0,
scale: 1
scale: 1,
displacement: options.displacement !== undefined ? options.displacement : [0, 0]
});
/**
@@ -160,7 +162,8 @@ class RegularShape extends ImageStyle {
angle: this.getAngle(),
stroke: this.getStroke() ? this.getStroke().clone() : undefined,
rotation: this.getRotation(),
rotateWithView: this.getRotateWithView()
rotateWithView: this.getRotateWithView(),
displacement: this.getDisplacement().slice()
});
style.setOpacity(this.getOpacity());
style.setScale(this.getScale());
@@ -353,12 +356,13 @@ class RegularShape extends ImageStyle {
// canvas.width and height are rounded to the closest integer
size = this.canvas_.width;
const imageSize = size;
const displacement = this.getDisplacement();
this.draw_(renderOptions, context, 0, 0);
this.createHitDetectionCanvas_(renderOptions);
this.anchor_ = [size / 2, size / 2];
this.anchor_ = [size / 2 - displacement[0], size / 2 + displacement[1]];
this.size_ = [size, size];
this.imageSize_ = [imageSize, imageSize];
}

View File

@@ -13,9 +13,10 @@ import Stroke from './Stroke.js';
* A function that takes an {@link module:ol/Feature} and a `{number}`
* representing the view's resolution. The function should return a
* {@link module:ol/style/Style} or an array of them. This way e.g. a
* vector layer can be styled.
* vector layer can be styled. If the function returns `undefined`, the
* feature will not be rendered.
*
* @typedef {function(import("../Feature.js").FeatureLike, number):(Style|Array<Style>)} StyleFunction
* @typedef {function(import("../Feature.js").FeatureLike, number):(Style|Array<Style>|void)} StyleFunction
*/
/**

View File

@@ -72,8 +72,9 @@ export function createForExtent(extent, opt_maxZoom, opt_tileSize, opt_corner) {
/**
* @typedef {Object} XYZOptions
* @property {import("./extent.js").Extent} [extent] Extent for the tile grid. The origin for an XYZ tile grid is the
* top-left corner of the extent. The zero level of the grid is defined by the resolution at which one tile fits in the
* provided extent. If not provided, the extent of the EPSG:3857 projection is used.
* top-left corner of the extent. If `maxResolution` is not provided the zero level of the grid is defined by the resolution
* at which one tile fits in the provided extent. If not provided, the extent of the EPSG:3857 projection is used.
* @property {number} [maxResolution] Resolution at level zero.
* @property {number} [maxZoom] Maximum zoom. The default is `42`. This determines the number of levels
* in the grid set. For example, a `maxZoom` of 21 means there are 22 levels in the grid set.
* @property {number} [minZoom=0] Minimum zoom.
@@ -99,7 +100,8 @@ export function createXYZ(opt_options) {
resolutions: resolutionsFromExtent(
extent,
xyzOptions.maxZoom,
xyzOptions.tileSize
xyzOptions.tileSize,
xyzOptions.maxResolution
)
};
return new TileGrid(gridOptions);
@@ -113,9 +115,10 @@ export function createXYZ(opt_options) {
* DEFAULT_MAX_ZOOM).
* @param {number|import("./size.js").Size=} opt_tileSize Tile size (default uses
* DEFAULT_TILE_SIZE).
* @param {number=} opt_maxResolution Resolution at level zero.
* @return {!Array<number>} Resolutions array.
*/
function resolutionsFromExtent(extent, opt_maxZoom, opt_tileSize) {
function resolutionsFromExtent(extent, opt_maxZoom, opt_tileSize, opt_maxResolution) {
const maxZoom = opt_maxZoom !== undefined ?
opt_maxZoom : DEFAULT_MAX_ZOOM;
@@ -124,8 +127,8 @@ function resolutionsFromExtent(extent, opt_maxZoom, opt_tileSize) {
const tileSize = toSize(opt_tileSize !== undefined ?
opt_tileSize : DEFAULT_TILE_SIZE);
const maxResolution = Math.max(
width / tileSize[0], height / tileSize[1]);
const maxResolution = opt_maxResolution > 0 ? opt_maxResolution :
Math.max(width / tileSize[0], height / tileSize[1]);
const length = maxZoom + 1;
const resolutions = new Array(length);

View File

@@ -558,6 +558,7 @@ class WebGLHelper extends Disposable {
if (value instanceof HTMLCanvasElement || value instanceof HTMLImageElement || value instanceof ImageData) {
// create a texture & put data
if (!uniform.texture) {
uniform.prevValue = undefined;
uniform.texture = gl.createTexture();
}
gl.activeTexture(gl[`TEXTURE${textureSlot}`]);
@@ -567,7 +568,8 @@ class WebGLHelper extends Disposable {
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
const imageReady = !(value instanceof HTMLImageElement) || /** @type {HTMLImageElement} */(value).complete;
if (imageReady) {
if (imageReady && uniform.prevValue !== value) {
uniform.prevValue = value;
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, value);
}

View File

@@ -427,6 +427,7 @@ export function parseLiteralStyle(style) {
const parsedSize = expressionToGlsl(vertContext, size, ValueTypes.NUMBER_ARRAY | ValueTypes.NUMBER);
const parsedOffset = expressionToGlsl(vertContext, offset, ValueTypes.NUMBER_ARRAY);
const parsedTexCoord = expressionToGlsl(vertContext, texCoord, ValueTypes.NUMBER_ARRAY);
const parsedRotation = expressionToGlsl(vertContext, rotation, ValueTypes.NUMBER);
/**
* @type {import("../style/expressions.js").ParsingContext}
@@ -439,7 +440,6 @@ export function parseLiteralStyle(style) {
};
const parsedColor = expressionToGlsl(fragContext, color, ValueTypes.COLOR);
const parsedOpacity = expressionToGlsl(fragContext, opacity, ValueTypes.NUMBER);
const parsedRotation = expressionToGlsl(fragContext, rotation, ValueTypes.NUMBER);
let opacityFilter = '1.0';
const visibleSize = `vec2(${expressionToGlsl(fragContext, size, ValueTypes.NUMBER_ARRAY | ValueTypes.NUMBER)}).x`;