diff --git a/CHANGELOG.md b/CHANGELOG.md index f0a9646174..6c03b18e23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## 4.7.1-beta.1 + +- Added custom features that were not part of the main branch + - latitude bounds + - support for texture formats other than gl.RGBA used by raster tile sources + - exports for Event, Tile, SourceCache, and OverscaledTileID + +# === LeafletGL follows === + ## main ### ✨ Features and improvements diff --git a/package.json b/package.json index 0bc77623bc..bae96eab11 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "maplibre-gl", + "name": "@windycom/maplibre-gl", "description": "BSD licensed community fork of mapbox-gl, a WebGL interactive maps library", "version": "5.0.0-pre.1", "main": "dist/maplibre-gl.js", @@ -189,4 +189,4 @@ "npm": ">=8.1.0", "node": ">=16.14.0" } -} +} \ No newline at end of file diff --git a/src/geo/lng_lat_bounds.ts b/src/geo/lng_lat_bounds.ts index c3f1399384..0ecba74e55 100644 --- a/src/geo/lng_lat_bounds.ts +++ b/src/geo/lng_lat_bounds.ts @@ -23,6 +23,8 @@ export type LngLatBoundsLike = LngLatBounds | [LngLatLike, LngLatLike] | [number * A `LngLatBounds` object represents a geographical bounding box, * defined by its southwest and northeast points in longitude and latitude. * + * If the longitude bounds difference is over 360°, no longitude bound is applied. + * * If no arguments are provided to the constructor, a `null` bounding box is created. * * Note that any Mapbox GL method that accepts a `LngLatBounds` object as an argument or option diff --git a/src/geo/projection/mercator_transform.ts b/src/geo/projection/mercator_transform.ts index 0347a4ecd6..83b945c397 100644 --- a/src/geo/projection/mercator_transform.ts +++ b/src/geo/projection/mercator_transform.ts @@ -449,7 +449,9 @@ export class MercatorTransform implements ITransform { if (shouldZoomIn) scaleY = screenHeight / (maxY - minY); } - if (lngRange) { + const lngUnlimited = !lngRange || Math.abs(lngRange[0] - lngRange[1]) >= 1e6; + + if (lngRange && !lngUnlimited) { minX = wrap( mercatorXfromLng(lngRange[0]) * worldSize, 0, @@ -488,7 +490,7 @@ export class MercatorTransform implements ITransform { if (originalY + h2 > maxY) modifiedY = maxY - h2; } - if (lngRange) { + if (lngRange && !lngUnlimited) { const centerX = (minX + maxX) / 2; let wrappedX = originalX; if (this._helper._renderWorldCopies) { diff --git a/src/geo/transform_helper.ts b/src/geo/transform_helper.ts index 7a845ea456..d45ac78a42 100644 --- a/src/geo/transform_helper.ts +++ b/src/geo/transform_helper.ts @@ -420,7 +420,7 @@ export class TransformHelper implements ITransformGetters { setMaxBounds(bounds?: LngLatBounds | null): void { if (bounds) { this._lngRange = [bounds.getWest(), bounds.getEast()]; - this._latRange = [bounds.getSouth(), bounds.getNorth()]; + this._latRange = [Math.max(bounds.getSouth(), -MAX_VALID_LATITUDE), Math.min(bounds.getNorth(), MAX_VALID_LATITUDE)]; this._constrain(); } else { this._lngRange = null; diff --git a/src/index.ts b/src/index.ts index 7b046a663e..73fcf3b703 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import packageJSON from '../package.json' with {type: 'json'}; +import packageJSON from '../package.json' with { type: 'json' }; import {Map} from './ui/map'; import {NavigationControl} from './ui/control/navigation_control'; import {GeolocateControl} from './ui/control/geolocate_control'; @@ -14,7 +14,7 @@ import {LngLat, LngLatLike} from './geo/lng_lat'; import {LngLatBounds, LngLatBoundsLike} from './geo/lng_lat_bounds'; import Point from '@mapbox/point-geometry'; import {MercatorCoordinate} from './geo/mercator_coordinate'; -import {Evented} from './util/evented'; +import {Evented, Event} from './util/evented'; import {config} from './util/config'; import {rtlMainThreadPluginFactory} from './source/rtl_text_plugin_main_thread'; import {WorkerPool} from './util/worker_pool'; @@ -27,6 +27,8 @@ import {RasterDEMTileSource} from './source/raster_dem_tile_source'; import {RasterTileSource} from './source/raster_tile_source'; import {VectorTileSource} from './source/vector_tile_source'; import {VideoSource} from './source/video_source'; +import {OverscaledTileID} from './source/tile_id'; +import {Tile} from './source/tile'; import {Source, addSourceType} from './source/source'; import {addProtocol, removeProtocol} from './source/protocol_crud'; import {getGlobalDispatcher} from './util/dispatcher'; @@ -47,6 +49,7 @@ import {KeyboardHandler} from './ui/handler/keyboard'; import {TwoFingersTouchPitchHandler, TwoFingersTouchRotateHandler, TwoFingersTouchZoomHandler} from './ui/handler/two_fingers_touch'; import {MessageType} from './util/actor_messages'; import {createTileMesh} from './util/create_tile_mesh'; +import {SourceCache} from './source/source_cache'; const version = packageJSON.version; export type * from '@maplibre/maplibre-gl-style-spec'; @@ -188,6 +191,9 @@ export { LngLatBounds, Point, MercatorCoordinate, + SourceCache, + Event, + Tile, Evented, AJAXError, config, @@ -213,6 +219,7 @@ export { MapWheelEvent, MapTouchEvent, MapMouseEvent, + OverscaledTileID, type IControl, type CustomLayerInterface, type CanvasSourceSpecification, diff --git a/src/render/texture.ts b/src/render/texture.ts index 99ce6a335f..f568fb5ca3 100644 --- a/src/render/texture.ts +++ b/src/render/texture.ts @@ -1,8 +1,9 @@ import type {Context} from '../gl/context'; import type {RGBAImage, AlphaImage} from '../util/image'; -import {isImageBitmap} from '../util/util'; +import {extend, isImageBitmap} from '../util/util'; -export type TextureFormat = WebGLRenderingContextBase['RGBA'] | WebGLRenderingContextBase['ALPHA']; +export type TextureFormatWebGL2 = WebGL2RenderingContextBase['RG8'] | WebGL2RenderingContextBase['R8'] +export type TextureFormat = WebGLRenderingContextBase['RGBA'] |WebGLRenderingContextBase['RGB'] | WebGLRenderingContextBase['ALPHA'] | WebGLRenderingContextBase['LUMINANCE'] | TextureFormatWebGL2; export type TextureFilter = WebGLRenderingContextBase['LINEAR'] | WebGLRenderingContextBase['LINEAR_MIPMAP_NEAREST'] | WebGLRenderingContextBase['NEAREST']; export type TextureWrap = WebGLRenderingContextBase['REPEAT'] | WebGLRenderingContextBase['CLAMP_TO_EDGE'] | WebGLRenderingContextBase['MIRRORED_REPEAT']; @@ -35,12 +36,18 @@ export class Texture { this.context = context; this.format = format; this.texture = context.gl.createTexture(); - this.update(image, options); + + // Pass format to the update method to enforce its usage + this.update(image, options ? extend(options, {format}) : {format}); } + /** + * Updates texture content, can also change texture format if necessary + */ update(image: TextureImage, options?: { premultiply?: boolean; useMipmap?: boolean; + format?:TextureFormat; } | null, position?: { x: number; y: number; @@ -51,27 +58,36 @@ export class Texture { const {gl} = context; this.useMipmap = Boolean(options && options.useMipmap); + + // Use default maplibre format gl.RGBA to remain compatible with all users of the Texture class + const newFormat = options?.format ?? gl.RGBA; + const formatChanged = this.format !== newFormat; + this.format = newFormat; + gl.bindTexture(gl.TEXTURE_2D, this.texture); context.pixelStoreUnpackFlipY.set(false); context.pixelStoreUnpack.set(1); context.pixelStoreUnpackPremultiplyAlpha.set(this.format === gl.RGBA && (!options || options.premultiply !== false)); - if (resize) { + // Since internal-format and format can be represented by different values (e.g. gl.RG8 vs gl.RG) in WebGL2, we need to preform conversion + const format = this.textureFormatFromInternalFormat(this.format); + + if (resize || formatChanged) { this.size = [width, height]; if (image instanceof HTMLImageElement || image instanceof HTMLCanvasElement || image instanceof HTMLVideoElement || image instanceof ImageData || isImageBitmap(image)) { - gl.texImage2D(gl.TEXTURE_2D, 0, this.format, this.format, gl.UNSIGNED_BYTE, image); + gl.texImage2D(gl.TEXTURE_2D, 0, this.format, format, gl.UNSIGNED_BYTE, image); } else { - gl.texImage2D(gl.TEXTURE_2D, 0, this.format, width, height, 0, this.format, gl.UNSIGNED_BYTE, (image as DataTextureImage).data); + gl.texImage2D(gl.TEXTURE_2D, 0, this.format, width, height, 0, format, gl.UNSIGNED_BYTE, (image as DataTextureImage).data); } } else { const {x, y} = position || {x: 0, y: 0}; if (image instanceof HTMLImageElement || image instanceof HTMLCanvasElement || image instanceof HTMLVideoElement || image instanceof ImageData || isImageBitmap(image)) { - gl.texSubImage2D(gl.TEXTURE_2D, 0, x, y, gl.RGBA, gl.UNSIGNED_BYTE, image); + gl.texSubImage2D(gl.TEXTURE_2D, 0, x, y, format, gl.UNSIGNED_BYTE, image); } else { - gl.texSubImage2D(gl.TEXTURE_2D, 0, x, y, width, height, gl.RGBA, gl.UNSIGNED_BYTE, (image as DataTextureImage).data); + gl.texSubImage2D(gl.TEXTURE_2D, 0, x, y, width, height, format, gl.UNSIGNED_BYTE, (image as DataTextureImage).data); } } @@ -111,4 +127,26 @@ export class Texture { gl.deleteTexture(this.texture); this.texture = null; } + + /** + * Method for accessing texture format by its internal format for cases, when these two are not the same + * - specifically for special WebGL2 formats + */ + textureFormatFromInternalFormat(internalFormat: TextureFormat) { + let format: GLenum = internalFormat; + + if (!WebGL2RenderingContext) { + return format; + } + + switch (internalFormat) { + case WebGL2RenderingContext['RG8']: + format = WebGL2RenderingContext['RG']; + break; + case WebGL2RenderingContext['R8']: + format = WebGL2RenderingContext['RED']; + break; + } + return format; + } } diff --git a/src/source/raster_tile_source.ts b/src/source/raster_tile_source.ts index 7ad46cb0bd..e29157e05a 100644 --- a/src/source/raster_tile_source.ts +++ b/src/source/raster_tile_source.ts @@ -6,7 +6,7 @@ import {ResourceType} from '../util/request_manager'; import {Event, ErrorEvent, Evented} from '../util/evented'; import {loadTileJson} from './load_tilejson'; import {TileBounds} from './tile_bounds'; -import {Texture} from '../render/texture'; +import {Texture, TextureFormat} from '../render/texture'; import type {Source} from './source'; import type {OverscaledTileID} from './tile_id'; @@ -66,6 +66,7 @@ export class RasterTileSource extends Evented implements Source { _loaded: boolean; _options: RasterSourceSpecification | RasterDEMSourceSpecification; _tileJSONRequest: AbortController; + _textureFormat: TextureFormat; constructor(id: string, options: RasterSourceSpecification | RasterDEMSourceSpecification, dispatcher: Dispatcher, eventedParent: Evented) { super(); @@ -80,11 +81,20 @@ export class RasterTileSource extends Evented implements Source { this.scheme = 'xyz'; this.tileSize = 512; this._loaded = false; + this._textureFormat = WebGLRenderingContext.RGBA; this._options = extend({type: 'raster'}, options); extend(this, pick(options, ['url', 'scheme', 'tileSize'])); } + set textureFormat(format: TextureFormat) { + this._textureFormat = format; + } + + get textureFormat(): TextureFormat { + return this._textureFormat; + } + async load() { this._loaded = false; this.fire(new Event('dataloading', {dataType: 'source'})); @@ -189,10 +199,12 @@ export class RasterTileSource extends Evented implements Source { const gl = context.gl; const img = response.data; tile.texture = this.map.painter.getTileTexture(img.width); + if (tile.texture) { - tile.texture.update(img, {useMipmap: true}); + // We need to update the format since tile textures are being reused by various sources + tile.texture.update(img, {useMipmap: true, format: this.textureFormat}); } else { - tile.texture = new Texture(context, img, gl.RGBA, {useMipmap: true}); + tile.texture = new Texture(context, img, this.textureFormat, {useMipmap: true}); tile.texture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE, gl.LINEAR_MIPMAP_NEAREST); } tile.state = 'loaded'; diff --git a/src/util/web_worker_transfer.test.ts b/src/util/web_worker_transfer.test.ts index 4969e0dc97..ce9d4db68e 100644 --- a/src/util/web_worker_transfer.test.ts +++ b/src/util/web_worker_transfer.test.ts @@ -1,6 +1,5 @@ -import {SerializedObject} from '../../dist/maplibre-gl'; import {AJAXError} from './ajax'; -import {register, serialize, deserialize} from './web_worker_transfer'; +import {register, serialize, deserialize, SerializedObject} from './web_worker_transfer'; describe('web worker transfer', () => { test('round trip', () => { diff --git a/src/util/web_worker_transfer.ts b/src/util/web_worker_transfer.ts index a8f1cd4325..3616156959 100644 --- a/src/util/web_worker_transfer.ts +++ b/src/util/web_worker_transfer.ts @@ -7,7 +7,7 @@ import {isImageBitmap} from './util'; /** * A class that is serialized to and json, that can be constructed back to the original class in the worker or in the main thread */ -type SerializedObject = { +export type SerializedObject = { [_: string]: S; }; diff --git a/test/build/min.test.ts b/test/build/min.test.ts index 71272d2f14..bac948a859 100644 --- a/test/build/min.test.ts +++ b/test/build/min.test.ts @@ -36,7 +36,7 @@ describe('test min build', () => { const decreaseQuota = 4096; // feel free to update this value after you've checked that it has changed on purpose :-) - const expectedBytes = 877777; + const expectedBytes = 879038; expect(actualBytes).toBeLessThan(expectedBytes + increaseQuota); expect(actualBytes).toBeGreaterThan(expectedBytes - decreaseQuota);