import * as mercator from "../mercator";
import { BaseTileMaterialLayer } from "./BaseTileMaterialLayer";
import type { IBaseTileMaterialLayerParams } from "./BaseTileMaterialLayer";
import type { LayerEventsList } from "./Layer";
import { RENDERING } from "../quadTree/quadTree";
import { Segment } from "../segment/Segment";
import { stringTemplate } from "../utils/shared";
import type { EventsHandler } from "../Events";
import { Material } from "./Material";
import type { FetchCache, IResponse } from "../utils/Loader";
export interface IXYZParams extends IBaseTileMaterialLayerParams {
url?: string;
subdomains?: string[];
minNativeZoom?: number;
maxNativeZoom?: number;
urlRewrite?: Function;
/**
* Fetch RequestCache value
* https://developer.mozilla.org/en-US/docs/Web/API/Request/cache
* @default "default"
*/
cache?: FetchCache;
}
type XYZEventsList = ["load", "loadend"];
type XYZEventsType = EventsHandler<XYZEventsList> & EventsHandler<LayerEventsList>;
/**
* Represents an imagery tiles source provider.
* @class
* @extends {Layer}
* @param {string} name - Layer name.
* @param {IXYZParams} options:
* @param {number} [options.opacity=1.0] - Layer opacity.
* @param {Array.<string>} [options.subdomains=['a','b','c']] - Subdomains of the tile service.
* @param {number} [options.minZoom=0] - Minimal visibility zoom level.
* @param {number} [options.maxZoom=0] - Maximal visibility zoom level.
* @param {number} [options.minNativeZoom=0] - Minimal available zoom level.
* @param {number} [options.maxNativeZoom=19] - Maximal available zoom level.
* @param {string} [options.attribution] - Layer attribution shown in the attribution area.
* @param {boolean} [options.isBaseLayer=false] - Base layer flag.
* @param {boolean} [options.visibility=true] - Layer visibility.
* @param {string} [options.crossOrigin=true] - If true, all tiles will have their crossOrigin attribute set to ''.
* @param {string} options.url - Tile url source template(see example below).
* @param {string} options.textureFilter - Texture WebGL filter: NEAREST, LINEAR, MIPMAP, ANISOTROPIC.
* @param {Function} options.urlRewrite - URL rewrite function.
*
* @fires load
* @fires loadend
*
* @example <caption>Creates OpenStreetMap base tile layer</caption>
* new og.layer.XYZ("OpenStreetMap", {
* isBaseLayer: true,
* url: "http://b.tile.openstreetmap.org/{z}/{x}/{y}.png",
* visibility: true,
* attribution: 'Data @ <a href="http://www.openstreetmap.org/">OpenStreetMap</a> contributors, <a href="http://www.openstreetmap.org/copyright">ODbL</a>'
* });
*/
export class XYZ extends BaseTileMaterialLayer {
public override events: XYZEventsType;
/**
* Tile url source template.
* @public
* @type {string}
*/
public url: string;
/**
* @protected
*/
protected _s: string[];
/**
* Rewrites imagery tile url query.
* @private
* @param {Segment} segment - Segment to load.
* @param {string} url - Created url.
* @returns {string} URL query string.
*/
protected _urlRewriteCallback: Function | null;
protected _requestsPeerSubdomains: number;
protected _requestCount: number;
protected _cache: FetchCache;
constructor(name: string | null, options: IXYZParams = {}) {
super(name, options);
//@ts-ignore
this.events = this.events.registerNames(XYZ_EVENTS);
this.url = options.url || "";
this._s = options.subdomains || ["a", "b", "c"];
this.minNativeZoom = options.minNativeZoom || 0;
this.maxNativeZoom = options.maxNativeZoom || 19;
this._urlRewriteCallback = options.urlRewrite || null;
this._requestsPeerSubdomains = 4;
this._requestCount = 0;
this._cache = options.cache || "default";
}
/**
* @warning Use XYZ.isIdle in requestAnimationFrame(after setVisibility)
*/
public override get isIdle(): boolean {
return super.isIdle && this._planet!._tileLoader.getRequestCounter(this) === 0;
}
public override get instanceName(): string {
return "XYZ";
}
/**
* Abort loading tiles.
* @public
*/
public override abortLoading() {
if (this._planet) {
this._planet._tileLoader.abort(this);
}
}
/**
* Sets layer visibility.
* @public
* @param {boolean} visibility - Layer visibility.
*/
public override setVisibility(visibility: boolean) {
if (visibility !== this._visibility) {
super.setVisibility(visibility);
if (!visibility) {
this.abortLoading();
}
}
}
public override remove(): this {
this.abortLoading();
super.remove();
return this;
}
/**
* Sets imagery tiles url source template.
* @public
* @param {string} url - Url template.
* @example
* http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png
* where {z}, {x} and {y} - replaces by current tile values, {s} - random domain.
*/
public setUrl(url: string) {
this.url = url;
}
public _checkSegment(segment: Segment) {
return segment._projection.id === this._planet!.quadTreeStrategy.projection.id;
}
/**
* Start to load tile material.
* @public
* @virtual
* @param {Material} material - Loads current material.
* @param {boolean} [forceLoading=false] -
*/
public override loadMaterial(material: Material, forceLoading: boolean = false) {
let seg = material.segment;
if (this._isBaseLayer) {
material.texture = seg.getDefaultTexture();
} else {
material.texture = seg.planet.transparentTexture;
}
// Q: Maybe we should change "<2" to material.segment.tileZoom < (material.layer.minZoom + 1)
if (this._planet!.layerLock.isFree() || material.segment.tileZoom < 2) {
material.isReady = false;
material.isLoading = true;
if (this._checkSegment(seg)) {
material.loadingAttempts++;
this._planet!._tileLoader.load(
{
sender: this,
src: this._getHTTPRequestString(material.segment),
type: "imageBitmap",
filter: () => (seg.initialized && seg.node.getState() === RENDERING) || forceLoading,
options: {
cache: this._cache
}
},
(response: IResponse) => {
if (response.status === "ready") {
if (material.isLoading) {
let e = this.events.load!;
if (e.handlers.length) {
this.events.dispatch(e, material);
}
material.applyImage(response.data);
response.data = null;
}
} else if (response.status === "abort") {
material.isLoading = false;
} else if (response.status === "error") {
if (material.isLoading) {
material.textureNotExists();
}
}
}
);
} else {
material.textureNotExists();
}
}
}
/**
* Creates query url.
* @protected
* @virtual
* @param {Segment} segment - Creates specific url for current segment.
* @returns {string} URL string.
*/
protected _createUrl(segment: Segment): string {
return stringTemplate(this.url, {
s: this._getSubdomain(),
x: segment.tileX.toString(),
y: segment.tileY.toString(),
z: segment.tileZoom.toString()
});
}
protected _getSubdomain(): string {
this._requestCount++;
return this._s[
Math.floor(
(this._requestCount % (this._requestsPeerSubdomains * this._s.length)) / this._requestsPeerSubdomains
)
];
}
/**
* Returns actual url query string.
* @protected
* @param {Segment} segment - Segment that loads image data.
* @returns {string} URL string.
*/
protected _getHTTPRequestString(segment: Segment) {
return this._urlRewriteCallback ? this._urlRewriteCallback(segment, this.url) : this._createUrl(segment);
}
/**
* Sets url rewrite callback, used for custom url rewriting for every tile loading.
* @public
* @param {Function} ur - The callback that returns tile custom created url.
*/
public setUrlRewriteCallback(ur: Function) {
this._urlRewriteCallback = ur;
}
//
// Befor baseTileMaterialLayer was added
//
// public override applyMaterial(material: Material, forceLoading: boolean = false): NumberArray4 {
// if (this.waitForParentMaterial) {
// return this._apllyMaterialDefault(material, forceLoading);
// } else {
// return this._applyMaterialFast(material, forceLoading);
// }
// }
//
// protected _apllyMaterialDefault(material: Material, forceLoading: boolean = false): NumberArray4 {
// if (material.isReady) {
// return material.texOffset;
// } else if (material.segment.tileZoom < this.minNativeZoom) {
// material.textureNotExists();
// } else {
// let segment = material.segment;
// let layerId = this.__id;
//
// if (segment.passReady) {
// let node = segment.node;
// let targetNode = null;
//
// while (node) {
// const seg = node.segment;
//
// if (seg.tileZoom <= this.maxNativeZoom) {
// const mat = seg.materials[layerId];
// if (!mat || !mat.isReady) {
// targetNode = node;
// }
// }
//
// node = node.parentNode!;
// }
//
// if (targetNode) {
// const seg = targetNode.segment;
//
// let mat = seg.materials[layerId];
// if (!mat) {
// mat = seg.materials[layerId] = this.createMaterial(seg);
// }
//
// if (!mat.isReady && !mat.isLoading) {
// this.loadMaterial(mat, targetNode === segment.node ? forceLoading : true);
// }
// }
// }
//
// let pn = segment.node;
// let psegm: Material | null = null;
// while (pn) {
// const pm = pn.segment.materials[layerId];
// if (pm && pm.isReady && pm.textureExists) {
// psegm = pm;
// break;
// }
// pn = pn.parentNode!;
// }
//
// if (psegm && pn) {
// material.appliedNode = pn;
// material.appliedNodeId = pn.nodeId;
// material.texture = psegm.texture;
// let dZ2 = 1.0 / (2 << (segment.tileZoom - pn.segment.tileZoom - 1));
// material.texOffset[0] = segment.tileX * dZ2 - pn.segment.tileX;
// material.texOffset[1] = segment.tileY * dZ2 - pn.segment.tileY;
// material.texOffset[2] = dZ2;
// material.texOffset[3] = dZ2;
// } else {
// material.texture = segment.planet.transparentTexture;
// material.texOffset[0] = 0.0;
// material.texOffset[1] = 0.0;
// material.texOffset[2] = 1.0;
// material.texOffset[3] = 1.0;
// }
// }
//
// return material.texOffset;
// }
//
// protected _applyMaterialFast(material: Material, forceLoading: boolean = false): NumberArray4 {
// if (material.isReady) {
// return material.texOffset;
// } else if (material.segment.tileZoom < this.minNativeZoom) {
// material.textureNotExists();
// } else {
// let segment = material.segment,
// pn = segment.node,
// notEmpty = false;
//
// let mId = this.__id;
// let psegm = material;
// while (pn.parentNode) {
// pn = pn.parentNode;
// psegm = pn.segment.materials[mId];
// if (psegm && psegm.textureExists) {
// notEmpty = true;
// break;
// }
// }
//
// if (segment.passReady) {
// let maxNativeZoom = (material.layer as XYZ).maxNativeZoom;
// if (pn.segment.tileZoom === maxNativeZoom) {
// material.textureNotExists();
// } else if (material.segment.tileZoom <= maxNativeZoom) {
// !material.isLoading && !material.isReady && this.loadMaterial(material, forceLoading);
// } else {
// let pn = segment.node;
// while (pn.segment.tileZoom > (material.layer as XYZ).maxNativeZoom) {
// pn = pn.parentNode!;
// }
// let pnm = pn.segment.materials[material.layer.__id];
// if (pnm) {
// !pnm.isLoading && !pnm.isReady && this.loadMaterial(pnm, true);
// } else {
// pnm = pn.segment.materials[material.layer.__id] = material.layer.createMaterial(pn.segment);
// this.loadMaterial(pnm, true);
// }
// }
// }
//
// if (notEmpty) {
// material.appliedNode = pn;
// material.appliedNodeId = pn.nodeId;
// material.texture = psegm.texture;
// let dZ2 = 1.0 / (2 << (segment.tileZoom - pn.segment.tileZoom - 1));
// material.texOffset[0] = segment.tileX * dZ2 - pn.segment.tileX;
// material.texOffset[1] = segment.tileY * dZ2 - pn.segment.tileY;
// material.texOffset[2] = dZ2;
// material.texOffset[3] = dZ2;
// } else {
// material.texture = segment.planet.transparentTexture;
// material.texOffset[0] = 0.0;
// material.texOffset[1] = 0.0;
// material.texOffset[2] = 1.0;
// material.texOffset[3] = 1.0;
// }
// }
//
// return material.texOffset;
// }
/**
* @protected
*/
protected override _correctFullExtent() {
let e = this._extent,
em = this._extentMerc;
let ENLARGE_MERCATOR_LON = mercator.POLE + 50000;
let ENLARGE_MERCATOR_LAT = mercator.POLE + 50000;
if (e.northEast.lat === 90.0) {
em.northEast.lat = ENLARGE_MERCATOR_LAT;
}
if (e.northEast.lon === 180.0) {
em.northEast.lon = ENLARGE_MERCATOR_LON;
}
if (e.southWest.lat === -90.0) {
em.southWest.lat = -ENLARGE_MERCATOR_LAT;
}
if (e.southWest.lon === -180.0) {
em.southWest.lon = -ENLARGE_MERCATOR_LON;
}
// WHY!???
// if (e.northEast.lat >= mercator.MAX_LAT) {
// e.northEast.lat = mercator.MAX_LAT;
// }
//
// if (e.northEast.lat <= mercator.MIN_LAT) {
// e.northEast.lat = mercator.MIN_LAT;
// }
}
}
const XYZ_EVENTS: XYZEventsList = [
/**
* Triggered when current tile image has loaded before rendering.
* @event #load
*/
"load",
/**
* Triggered when all tiles have loaded or loading has stopped.
* @event #loadend
*/
"loadend"
];