Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
package.source.Raster.js Maven / Gradle / Ivy
/**
* @module ol/source/Raster
*/
import Disposable from '../Disposable.js';
import Event from '../events/Event.js';
import EventType from '../events/EventType.js';
import ImageCanvas from '../ImageCanvas.js';
import ImageLayer from '../layer/Image.js';
import ImageSource from './Image.js';
import Source from './Source.js';
import TileLayer from '../layer/Tile.js';
import TileQueue from '../TileQueue.js';
import TileSource from './Tile.js';
import {createCanvasContext2D} from '../dom.js';
import {create as createTransform} from '../transform.js';
import {equals, getCenter, getHeight, getWidth} from '../extent.js';
import {getUid} from '../util.js';
/**
* @typedef {Object} MinionData
* @property {Array} buffers Array of buffers.
* @property {Object} meta Operation metadata.
* @property {boolean} imageOps The operation is an image operation.
* @property {number} width The width of the image.
* @property {number} height The height of the image.
*/
/* istanbul ignore next */
/**
* Create a function for running operations. This function is serialized for
* use in a worker.
* @param {function(Array, Object):*} operation The operation.
* @return {function(MinionData):ArrayBuffer} A function that takes an object with
* buffers, meta, imageOps, width, and height properties and returns an array
* buffer.
*/
function createMinion(operation) {
return function (data) {
// bracket notation for minification support
const buffers = data['buffers'];
const meta = data['meta'];
const imageOps = data['imageOps'];
const width = data['width'];
const height = data['height'];
const numBuffers = buffers.length;
const numBytes = buffers[0].byteLength;
if (imageOps) {
const images = new Array(numBuffers);
for (let b = 0; b < numBuffers; ++b) {
images[b] = new ImageData(
new Uint8ClampedArray(buffers[b]),
width,
height,
);
}
const output = operation(images, meta).data;
return output.buffer;
}
const output = new Uint8ClampedArray(numBytes);
const arrays = new Array(numBuffers);
const pixels = new Array(numBuffers);
for (let b = 0; b < numBuffers; ++b) {
arrays[b] = new Uint8ClampedArray(buffers[b]);
pixels[b] = [0, 0, 0, 0];
}
for (let i = 0; i < numBytes; i += 4) {
for (let j = 0; j < numBuffers; ++j) {
const array = arrays[j];
pixels[j][0] = array[i];
pixels[j][1] = array[i + 1];
pixels[j][2] = array[i + 2];
pixels[j][3] = array[i + 3];
}
const pixel = operation(pixels, meta);
output[i] = pixel[0];
output[i + 1] = pixel[1];
output[i + 2] = pixel[2];
output[i + 3] = pixel[3];
}
return output.buffer;
};
}
/**
* Create a worker for running operations.
* @param {ProcessorOptions} config Processor options.
* @param {function(MessageEvent): void} onMessage Called with a message event.
* @return {Worker} The worker.
*/
function createWorker(config, onMessage) {
const lib = Object.keys(config.lib || {}).map(function (name) {
return 'const ' + name + ' = ' + config.lib[name].toString() + ';';
});
const lines = lib.concat([
'const __minion__ = (' + createMinion.toString() + ')(',
config.operation.toString(),
');',
'self.addEventListener("message", function(event) {',
' const buffer = __minion__(event.data);',
' self.postMessage({buffer: buffer, meta: event.data.meta}, [buffer]);',
'});',
]);
const worker = new Worker(
typeof Blob === 'undefined'
? 'data:text/javascript;base64,' +
Buffer.from(lines.join('\n'), 'binary').toString('base64')
: URL.createObjectURL(new Blob(lines, {type: 'text/javascript'})),
);
worker.addEventListener('message', onMessage);
return worker;
}
/**
* @typedef {Object} FauxMessageEvent
* @property {Object} data Message data.
*/
/**
* Create a faux worker for running operations.
* @param {ProcessorOptions} config Configuration.
* @param {function(FauxMessageEvent): void} onMessage Called with a message event.
* @return {Object} The faux worker.
*/
function createFauxWorker(config, onMessage) {
const minion = createMinion(config.operation);
let terminated = false;
return {
postMessage: function (data) {
setTimeout(function () {
if (terminated) {
return;
}
onMessage({data: {buffer: minion(data), meta: data['meta']}});
}, 0);
},
terminate: function () {
terminated = true;
},
};
}
/**
* @typedef {function(Error, ImageData, (Object|Array)): void} JobCallback
*/
/**
* @typedef {Object} Job
* @property {Object} meta Job metadata.
* @property {Array} inputs Array of input data.
* @property {JobCallback} callback Called when the job is complete.
*/
/**
* @typedef {Object} ProcessorOptions
* @property {number} threads Number of workers to spawn.
* @property {Operation} operation The operation.
* @property {Object} [lib] Functions that will be made available to operations run in a worker.
* @property {number} queue The number of queued jobs to allow.
* @property {boolean} [imageOps=false] Pass all the image data to the operation instead of a single pixel.
*/
/**
* @classdesc
* A processor runs pixel or image operations in workers.
*/
export class Processor extends Disposable {
/**
* @param {ProcessorOptions} config Configuration.
*/
constructor(config) {
super();
/**
* @type {boolean}
* @private
*/
this.imageOps_ = !!config.imageOps;
let threads;
if (config.threads === 0) {
threads = 0;
} else if (this.imageOps_) {
threads = 1;
} else {
threads = config.threads || 1;
}
/**
* @type {Array}
*/
const workers = new Array(threads);
if (threads) {
for (let i = 0; i < threads; ++i) {
workers[i] = createWorker(config, this.onWorkerMessage_.bind(this, i));
}
} else {
workers[0] = createFauxWorker(
config,
this.onWorkerMessage_.bind(this, 0),
);
}
/**
* @type {Array}
* @private
*/
this.workers_ = workers;
/**
* @type {Array}
* @private
*/
this.queue_ = [];
/**
* @type {number}
* @private
*/
this.maxQueueLength_ = config.queue || Infinity;
/**
* @type {number}
* @private
*/
this.running_ = 0;
/**
* @type {Object}
* @private
*/
this.dataLookup_ = {};
/**
* @type {Job|null}
* @private
*/
this.job_ = null;
}
/**
* Run operation on input data.
* @param {Array} inputs Array of image data.
* @param {Object} meta A user data object. This is passed to all operations
* and must be serializable.
* @param {function(Error, ImageData, Object): void} callback Called when work
* completes. The first argument is any error. The second is the ImageData
* generated by operations. The third is the user data object.
*/
process(inputs, meta, callback) {
this.enqueue_({
inputs: inputs,
meta: meta,
callback: callback,
});
this.dispatch_();
}
/**
* Add a job to the queue.
* @param {Job} job The job.
*/
enqueue_(job) {
this.queue_.push(job);
while (this.queue_.length > this.maxQueueLength_) {
this.queue_.shift().callback(null, null);
}
}
/**
* Dispatch a job.
*/
dispatch_() {
if (this.running_ || this.queue_.length === 0) {
return;
}
const job = this.queue_.shift();
this.job_ = job;
const width = job.inputs[0].width;
const height = job.inputs[0].height;
const buffers = job.inputs.map(function (input) {
return input.data.buffer;
});
const threads = this.workers_.length;
this.running_ = threads;
if (threads === 1) {
this.workers_[0].postMessage(
{
buffers: buffers,
meta: job.meta,
imageOps: this.imageOps_,
width: width,
height: height,
},
buffers,
);
return;
}
const length = job.inputs[0].data.length;
const segmentLength = 4 * Math.ceil(length / 4 / threads);
for (let i = 0; i < threads; ++i) {
const offset = i * segmentLength;
const slices = [];
for (let j = 0, jj = buffers.length; j < jj; ++j) {
slices.push(buffers[j].slice(offset, offset + segmentLength));
}
this.workers_[i].postMessage(
{
buffers: slices,
meta: job.meta,
imageOps: this.imageOps_,
width: width,
height: height,
},
slices,
);
}
}
/**
* Handle messages from the worker.
* @param {number} index The worker index.
* @param {MessageEvent} event The message event.
*/
onWorkerMessage_(index, event) {
if (this.disposed) {
return;
}
this.dataLookup_[index] = event.data;
--this.running_;
if (this.running_ === 0) {
this.resolveJob_();
}
}
/**
* Resolve a job. If there are no more worker threads, the processor callback
* will be called.
*/
resolveJob_() {
const job = this.job_;
const threads = this.workers_.length;
let data, meta;
if (threads === 1) {
data = new Uint8ClampedArray(this.dataLookup_[0]['buffer']);
meta = this.dataLookup_[0]['meta'];
} else {
const length = job.inputs[0].data.length;
data = new Uint8ClampedArray(length);
meta = new Array(threads);
const segmentLength = 4 * Math.ceil(length / 4 / threads);
for (let i = 0; i < threads; ++i) {
const buffer = this.dataLookup_[i]['buffer'];
const offset = i * segmentLength;
data.set(new Uint8ClampedArray(buffer), offset);
meta[i] = this.dataLookup_[i]['meta'];
}
}
this.job_ = null;
this.dataLookup_ = {};
job.callback(
null,
new ImageData(data, job.inputs[0].width, job.inputs[0].height),
meta,
);
this.dispatch_();
}
/**
* Terminate all workers associated with the processor.
* @override
*/
disposeInternal() {
for (let i = 0; i < this.workers_.length; ++i) {
this.workers_[i].terminate();
}
this.workers_.length = 0;
}
}
/**
* A function that takes an array of input data, performs some operation, and
* returns an array of output data.
* For `pixel` type operations, the function will be called with an array of
* pixels, where each pixel is an array of four numbers (`[r, g, b, a]`) in the
* range of 0 - 255. It should return a single pixel array.
* For `'image'` type operations, functions will be called with an array of
* [ImageData](https://developer.mozilla.org/en-US/docs/Web/API/ImageData)
* and should return a single
* [ImageData](https://developer.mozilla.org/en-US/docs/Web/API/ImageData).
* The operations
* are called with a second "data" argument, which can be used for storage. The
* data object is accessible from raster events, where it can be initialized in
* "beforeoperations" and accessed again in "afteroperations".
*
* @typedef {function((Array>|Array), Object):
* (Array|ImageData)} Operation
*/
/**
* @enum {string}
*/
const RasterEventType = {
/**
* Triggered before operations are run. Listeners will receive an event object with
* a `data` property that can be used to make data available to operations.
* @event module:ol/source/Raster.RasterSourceEvent#beforeoperations
* @api
*/
BEFOREOPERATIONS: 'beforeoperations',
/**
* Triggered after operations are run. Listeners will receive an event object with
* a `data` property. If more than one thread is used, `data` will be an array of
* objects. If a single thread is used, `data` will be a single object.
* @event module:ol/source/Raster.RasterSourceEvent#afteroperations
* @api
*/
AFTEROPERATIONS: 'afteroperations',
};
/**
* @typedef {'pixel' | 'image'} RasterOperationType
* Raster operation type. Supported values are `'pixel'` and `'image'`.
*/
/**
* @typedef {import("./Image.js").ImageSourceEventTypes|'beforeoperations'|'afteroperations'} RasterSourceEventTypes
*/
/**
* @classdesc
* Events emitted by {@link module:ol/source/Raster~RasterSource} instances are instances of this
* type.
*/
export class RasterSourceEvent extends Event {
/**
* @param {string} type Type.
* @param {import("../Map.js").FrameState} frameState The frame state.
* @param {Object|Array} data An object made available to operations. For "afteroperations" evenets
* this will be an array of objects if more than one thread is used.
*/
constructor(type, frameState, data) {
super(type);
/**
* The raster extent.
* @type {import("../extent.js").Extent}
* @api
*/
this.extent = frameState.extent;
/**
* The pixel resolution (map units per pixel).
* @type {number}
* @api
*/
this.resolution = frameState.viewState.resolution / frameState.pixelRatio;
/**
* An object made available to all operations. This can be used by operations
* as a storage object (e.g. for calculating statistics).
* @type {Object}
* @api
*/
this.data = data;
}
}
/**
* @typedef {Object} Options
* @property {Array} sources Input
* sources or layers. For vector data, use an VectorImage layer.
* @property {Operation} [operation] Raster operation.
* The operation will be called with data from input sources
* and the output will be assigned to the raster source.
* @property {Object} [lib] Functions that will be made available to operations run in a worker.
* @property {number} [threads] By default, operations will be run in a single worker thread.
* To avoid using workers altogether, set `threads: 0`. For pixel operations, operations can
* be run in multiple worker threads. Note that there is additional overhead in
* transferring data to multiple workers, and that depending on the user's
* system, it may not be possible to parallelize the work.
* @property {RasterOperationType} [operationType='pixel'] Operation type.
* Supported values are `'pixel'` and `'image'`. By default,
* `'pixel'` operations are assumed, and operations will be called with an
* array of pixels from input sources. If set to `'image'`, operations will
* be called with an array of ImageData objects from input sources.
* @property {Array|null} [resolutions] Resolutions. If specified, raster operations will only
* be run at the given resolutions. By default, the resolutions of the first source with resolutions
* specified will be used, if any. Set to `null` to use any view resolution instead.
*/
/***
* @template Return
* @typedef {import("../Observable").OnSignature &
* import("../Observable").OnSignature &
* import("../Observable").OnSignature &
* import("../Observable").OnSignature &
* import("../Observable").CombinedOnSignature} RasterSourceOnSignature
*/
/**
* @classdesc
* A source that transforms data from any number of input sources using an
* {@link module:ol/source/Raster~Operation} function to transform input pixel values into
* output pixel values.
*
* @fires module:ol/source/Raster.RasterSourceEvent
* @api
*/
class RasterSource extends ImageSource {
/**
* @param {Options} options Options.
*/
constructor(options) {
super({
projection: null,
});
/***
* @type {RasterSourceOnSignature}
*/
this.on;
/***
* @type {RasterSourceOnSignature}
*/
this.once;
/***
* @type {RasterSourceOnSignature}
*/
this.un;
/**
* @private
* @type {Processor}
*/
this.processor_ = null;
/**
* @private
* @type {RasterOperationType}
*/
this.operationType_ =
options.operationType !== undefined ? options.operationType : 'pixel';
/**
* @private
* @type {number}
*/
this.threads_ = options.threads !== undefined ? options.threads : 1;
/**
* @private
* @type {Array}
*/
this.layers_ = createLayers(options.sources);
const changed = this.changed.bind(this);
for (let i = 0, ii = this.layers_.length; i < ii; ++i) {
this.layers_[i].addEventListener(EventType.CHANGE, changed);
}
/**
* @private
* @type {boolean}
*/
this.useResolutions_ = options.resolutions !== null;
/**
* @private
* @type {import("../TileQueue.js").default}
*/
this.tileQueue_ = new TileQueue(function () {
return 1;
}, this.processSources_.bind(this));
/**
* The most recently requested frame state.
* @type {import("../Map.js").FrameState}
* @private
*/
this.requestedFrameState_;
/**
* The most recently rendered image canvas.
* @type {import("../ImageCanvas.js").default}
* @private
*/
this.renderedImageCanvas_ = null;
/**
* The most recently rendered revision.
* @type {number}
* @private
*/
this.renderedRevision_;
/**
* @private
* @type {import("../Map.js").FrameState}
*/
this.frameState_ = {
animate: false,
coordinateToPixelTransform: createTransform(),
declutter: null,
extent: null,
index: 0,
layerIndex: 0,
layerStatesArray: getLayerStatesArray(this.layers_),
pixelRatio: 1,
pixelToCoordinateTransform: createTransform(),
postRenderFunctions: [],
size: [0, 0],
tileQueue: this.tileQueue_,
time: Date.now(),
usedTiles: {},
viewState: /** @type {import("../View.js").State} */ ({
rotation: 0,
}),
viewHints: [],
wantedTiles: {},
mapId: getUid(this),
renderTargets: {},
};
this.setAttributions(function (frameState) {
/** @type {Array} */
const attributions = [];
for (let i = 0, iMax = options.sources.length; i < iMax; ++i) {
const sourceOrLayer = options.sources[i];
const source =
sourceOrLayer instanceof Source
? sourceOrLayer
: sourceOrLayer.getSource();
if (!source) {
continue;
}
const sourceAttributions = source.getAttributions()?.(frameState);
if (typeof sourceAttributions === 'string') {
attributions.push(sourceAttributions);
} else if (sourceAttributions !== undefined) {
attributions.push(...sourceAttributions);
}
}
return attributions;
});
if (options.operation !== undefined) {
this.setOperation(options.operation, options.lib);
}
}
/**
* Set the operation.
* @param {Operation} operation New operation.
* @param {Object} [lib] Functions that will be available to operations run
* in a worker.
* @api
*/
setOperation(operation, lib) {
if (this.processor_) {
this.processor_.dispose();
}
this.processor_ = new Processor({
operation: operation,
imageOps: this.operationType_ === 'image',
queue: 1,
lib: lib,
threads: this.threads_,
});
this.changed();
}
/**
* Update the stored frame state.
* @param {import("../extent.js").Extent} extent The view extent (in map units).
* @param {number} resolution The view resolution.
* @param {import("../proj/Projection.js").default} projection The view projection.
* @return {import("../Map.js").FrameState} The updated frame state.
* @private
*/
updateFrameState_(extent, resolution, projection) {
const frameState = /** @type {import("../Map.js").FrameState} */ (
Object.assign({}, this.frameState_)
);
frameState.viewState = /** @type {import("../View.js").State} */ (
Object.assign({}, frameState.viewState)
);
const center = getCenter(extent);
frameState.size[0] = Math.ceil(getWidth(extent) / resolution);
frameState.size[1] = Math.ceil(getHeight(extent) / resolution);
frameState.extent = [
center[0] - (frameState.size[0] * resolution) / 2,
center[1] - (frameState.size[1] * resolution) / 2,
center[0] + (frameState.size[0] * resolution) / 2,
center[1] + (frameState.size[1] * resolution) / 2,
];
frameState.time = Date.now();
const viewState = frameState.viewState;
viewState.center = center;
viewState.projection = projection;
viewState.resolution = resolution;
return frameState;
}
/**
* Determine if all sources are ready.
* @return {boolean} All sources are ready.
* @private
*/
allSourcesReady_() {
let ready = true;
let source;
for (let i = 0, ii = this.layers_.length; i < ii; ++i) {
source = this.layers_[i].getSource();
if (!source || source.getState() !== 'ready') {
ready = false;
break;
}
}
return ready;
}
/**
* @param {import("../extent.js").Extent} extent Extent.
* @param {number} resolution Resolution.
* @param {number} pixelRatio Pixel ratio.
* @param {import("../proj/Projection.js").default} projection Projection.
* @return {import("../ImageCanvas.js").default} Single image.
* @override
*/
getImage(extent, resolution, pixelRatio, projection) {
if (!this.allSourcesReady_()) {
return null;
}
this.tileQueue_.loadMoreTiles(16, 16);
resolution = this.findNearestResolution(resolution);
const frameState = this.updateFrameState_(extent, resolution, projection);
this.requestedFrameState_ = frameState;
// check if we can't reuse the existing ol/ImageCanvas
if (this.renderedImageCanvas_) {
const renderedResolution = this.renderedImageCanvas_.getResolution();
const renderedExtent = this.renderedImageCanvas_.getExtent();
if (
resolution !== renderedResolution ||
!equals(frameState.extent, renderedExtent)
) {
this.renderedImageCanvas_ = null;
}
}
if (
!this.renderedImageCanvas_ ||
this.getRevision() !== this.renderedRevision_
) {
this.processSources_();
}
if (frameState.animate) {
requestAnimationFrame(this.changed.bind(this));
}
return this.renderedImageCanvas_;
}
/**
* Start processing source data.
* @private
*/
processSources_() {
const frameState = this.requestedFrameState_;
const len = this.layers_.length;
const imageDatas = new Array(len);
for (let i = 0; i < len; ++i) {
frameState.layerIndex = i;
frameState.renderTargets = {};
const imageData = getImageData(this.layers_[i], frameState);
if (imageData) {
imageDatas[i] = imageData;
} else {
return;
}
}
const data = {};
this.dispatchEvent(
new RasterSourceEvent(RasterEventType.BEFOREOPERATIONS, frameState, data),
);
this.processor_.process(
imageDatas,
data,
this.onWorkerComplete_.bind(this, frameState),
);
}
/**
* Called when pixel processing is complete.
* @param {import("../Map.js").FrameState} frameState The frame state.
* @param {Error} err Any error during processing.
* @param {ImageData} output The output image data.
* @param {Object|Array} data The user data (or an array if more than one thread).
* @private
*/
onWorkerComplete_(frameState, err, output, data) {
if (err || !output) {
return;
}
// do nothing if extent or resolution changed
const extent = frameState.extent;
const resolution = frameState.viewState.resolution;
if (
resolution !== this.requestedFrameState_.viewState.resolution ||
!equals(extent, this.requestedFrameState_.extent)
) {
return;
}
let context;
if (this.renderedImageCanvas_) {
context = this.renderedImageCanvas_.getImage().getContext('2d');
} else {
const width = Math.round(getWidth(extent) / resolution);
const height = Math.round(getHeight(extent) / resolution);
context = createCanvasContext2D(width, height);
this.renderedImageCanvas_ = new ImageCanvas(
extent,
resolution,
1,
context.canvas,
);
}
context.putImageData(output, 0, 0);
if (frameState.animate) {
requestAnimationFrame(this.changed.bind(this));
} else {
this.changed();
}
this.renderedRevision_ = this.getRevision();
this.dispatchEvent(
new RasterSourceEvent(RasterEventType.AFTEROPERATIONS, frameState, data),
);
}
/**
* @param {import("../proj/Projection").default} [projection] Projection.
* @return {Array|null} Resolutions.
* @override
*/
getResolutions(projection) {
if (!this.useResolutions_) {
return null;
}
let resolutions = super.getResolutions();
if (!resolutions) {
for (let i = 0, ii = this.layers_.length; i < ii; ++i) {
const source = this.layers_[i].getSource();
resolutions = source.getResolutions(projection);
if (resolutions) {
break;
}
}
}
return resolutions;
}
/**
* @override
*/
disposeInternal() {
if (this.processor_) {
this.processor_.dispose();
}
super.disposeInternal();
}
}
/**
* Clean up and unregister the worker.
* @function
* @api
*/
RasterSource.prototype.dispose;
/**
* A reusable canvas context.
* @type {CanvasRenderingContext2D}
* @private
*/
let sharedContext = null;
/**
* Get image data from a layer.
* @param {import("../layer/Layer.js").default} layer Layer to render.
* @param {import("../Map.js").FrameState} frameState The frame state.
* @return {ImageData} The image data.
*/
function getImageData(layer, frameState) {
const renderer = layer.getRenderer();
if (!renderer) {
throw new Error('Unsupported layer type: ' + layer);
}
if (!renderer.prepareFrame(frameState)) {
return null;
}
const width = frameState.size[0];
const height = frameState.size[1];
if (width === 0 || height === 0) {
return null;
}
const container = renderer.renderFrame(frameState, null);
let element;
if (container instanceof HTMLCanvasElement) {
element = container;
} else {
if (container) {
element = container.firstElementChild;
}
if (!(element instanceof HTMLCanvasElement)) {
throw new Error('Unsupported rendered element: ' + element);
}
if (element.width === width && element.height === height) {
const context = element.getContext('2d');
return context.getImageData(0, 0, width, height);
}
}
if (!sharedContext) {
sharedContext = createCanvasContext2D(width, height, undefined, {
willReadFrequently: true,
});
} else {
const canvas = sharedContext.canvas;
if (canvas.width !== width || canvas.height !== height) {
sharedContext = createCanvasContext2D(width, height, undefined, {
willReadFrequently: true,
});
} else {
sharedContext.clearRect(0, 0, width, height);
}
}
sharedContext.drawImage(element, 0, 0, width, height);
return sharedContext.getImageData(0, 0, width, height);
}
/**
* Get a list of layer states from a list of layers.
* @param {Array} layers Layers.
* @return {Array} The layer states.
*/
function getLayerStatesArray(layers) {
return layers.map(function (layer) {
return layer.getLayerState();
});
}
/**
* Create layers for all sources.
* @param {Array} sources The sources.
* @return {Array} Array of layers.
*/
function createLayers(sources) {
const len = sources.length;
const layers = new Array(len);
for (let i = 0; i < len; ++i) {
layers[i] = createLayer(sources[i]);
}
return layers;
}
/**
* Create a layer for the provided source.
* @param {import("./Source.js").default|import("../layer/Layer.js").default} layerOrSource The layer or source.
* @return {import("../layer/Layer.js").default} The layer.
*/
function createLayer(layerOrSource) {
// @type {import("../layer/Layer.js").default}
let layer;
if (layerOrSource instanceof Source) {
if (layerOrSource instanceof TileSource) {
layer = new TileLayer({source: layerOrSource});
} else if (layerOrSource instanceof ImageSource) {
layer = new ImageLayer({source: layerOrSource});
}
} else {
layer = layerOrSource;
}
return layer;
}
export default RasterSource;