Created
June 15, 2024 18:33
-
-
Save laurenchen0631/2c0aeff205a2fc9c0cf64b5bdabd19d9 to your computer and use it in GitHub Desktop.
Pixi.ks v8 + Leaflet v1.9 in typescript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import L, { | |
LeafletEvent, | |
LeafletEventHandlerFn, | |
LeafletMouseEvent, | |
ZoomAnimEvent, | |
} from "leaflet"; | |
import { | |
AutoDetectOptions, | |
Container, | |
Renderer, | |
autoDetectRenderer, | |
} from "pixi.js"; | |
interface PixiOverlayOptions extends L.LayerOptions { | |
padding: number; | |
forceCanvas: boolean; | |
doubleBuffering: boolean; | |
resolution: number; | |
projectionZoom: (map: L.Map) => number; | |
destroyInteractionManager: boolean; | |
autoPreventDefault: boolean; | |
preserveDrawingBuffer: boolean; | |
clearBeforeRender: boolean; | |
shouldRedrawOnMove: (e: LeafletMouseEvent) => boolean; | |
} | |
class PixiOverlay extends L.Layer { | |
options: PixiOverlayOptions = { | |
// How much to extend the clip area around the map view (relative to its size) | |
// e.g. 0.1 would be 10% of map view in each direction | |
padding: 0.1, | |
// Force use of a 2d-canvas | |
forceCanvas: false, | |
// Help to prevent flicker when refreshing display on some devices (e.g. iOS devices) | |
// It is ignored if rendering is done with 2d-canvas | |
doubleBuffering: false, | |
// Resolution of the renderer canvas | |
resolution: L.Browser.retina ? 2 : 1, | |
// return the layer projection zoom level | |
projectionZoom(map: L.Map): number { | |
const maxZoom = map.getMaxZoom(); | |
const minZoom = map.getMinZoom(); | |
if (maxZoom === Infinity) return minZoom + 8; | |
return (maxZoom + minZoom) / 2; | |
}, | |
// Destroy PIXI EventSystem | |
destroyInteractionManager: false, | |
// Customize PIXI EventSystem autoPreventDefault property | |
// This option is ignored if destroyInteractionManager is set | |
autoPreventDefault: true, | |
// Enables drawing buffer preservation | |
preserveDrawingBuffer: false, | |
// Clear the canvas before the new render pass | |
clearBeforeRender: true, | |
// filter move events that should trigger a layer redraw | |
shouldRedrawOnMove: function () { | |
return false; | |
}, | |
}; | |
private mapSetting = { | |
initialZoom: 0, | |
wgsOrigin: L.latLng([0, 0]), | |
wgsInitialShift: L.point(0, 0), | |
mapInitialZoom: 0, | |
}; | |
private __map?: L.Map; | |
private domContainer?: HTMLElement; | |
private renderer?: Renderer; | |
private auxRenderer?: Renderer; | |
public bounds = new L.Bounds([0, 0], [0, 0]); | |
public center = new L.LatLng(0, 0); | |
public zoom = 0; | |
constructor( | |
public drawCallback: (renderer: Renderer, e: Partial<LeafletEvent>) => void, | |
private _container: Container, | |
options: Partial<PixiOverlayOptions> = {}, | |
) { | |
super(); | |
this.initialize(drawCallback, _container, options); | |
} | |
private get rendererOptions(): Partial<AutoDetectOptions> { | |
return { | |
resolution: this.options.resolution, | |
antialias: true, | |
// forceCanvas: this.options.forceCanvas, | |
preserveDrawingBuffer: this.options.preserveDrawingBuffer, | |
clearBeforeRender: this.options.clearBeforeRender, | |
backgroundAlpha: 0, | |
}; | |
} | |
private get doubleBuffering() { | |
return this.options.doubleBuffering && !this.options.forceCanvas; | |
} | |
public initialize( | |
drawCallback: (renderer: Renderer, e: Partial<LeafletEvent>) => void, | |
container: Container, | |
options: Partial<PixiOverlayOptions>, | |
) { | |
// merge options | |
L.setOptions(this, options); | |
// L.stamp(this); | |
this.drawCallback = drawCallback; | |
this._container = container; | |
} | |
public latLngToLayerPoint( | |
this: PixiOverlay, | |
latLng: Parameters<typeof L.latLng>[0], | |
_zoom?: number, | |
): L.Point { | |
if (!this.map) { | |
return L.point(0, 0); | |
} | |
const zoom = _zoom ?? this.mapSetting.initialZoom; | |
return this.map.project(L.latLng(latLng), zoom); | |
} | |
public layerPointToLatLng( | |
this: PixiOverlay, | |
point: L.PointTuple, | |
_zoom?: number, | |
): L.LatLng { | |
if (!this.map) { | |
return L.latLng(0, 0); | |
} | |
const zoom = _zoom ?? this.mapSetting.initialZoom; | |
return this.map.unproject(L.point(point), zoom); | |
} | |
public getScale(_zoom?: number): number { | |
const zoom = _zoom ?? this.map?.getZoom() ?? 1; | |
const { initialZoom } = this.mapSetting; | |
return this.map?.getZoomScale(zoom, initialZoom) ?? 1; | |
} | |
get map() { | |
return this.__map; | |
} | |
get container() { | |
return this._container; | |
} | |
// eslint-disable-next-line @typescript-eslint/no-explicit-any | |
async onAdd(map: L.Map): any { | |
this.__map = map; | |
await this.initializeRenderer(); | |
const initialZoom = this.options.projectionZoom(map); | |
const wgsOrigin = L.latLng([0, 0]); | |
this.mapSetting = { | |
initialZoom, | |
wgsOrigin, | |
wgsInitialShift: map.project(wgsOrigin, initialZoom), | |
mapInitialZoom: map.getZoom(), | |
}; | |
this.update({ | |
type: "add", | |
target: this, | |
sourceTarget: this, | |
}); | |
return this; | |
} | |
onRemove(): this { | |
this.__map = undefined; | |
if (this.domContainer) { | |
L.DomUtil.remove(this.domContainer); | |
} | |
return this; | |
} | |
destroy(): void { | |
this.remove(); | |
if (this.renderer) { | |
this.renderer.destroy(); | |
this.renderer = undefined; | |
} | |
if (this.auxRenderer) { | |
this.auxRenderer.destroy(); | |
this.auxRenderer = undefined; | |
} | |
} | |
getEvents(): { [name: string]: LeafletEventHandlerFn } { | |
return { | |
zoom: this.onZoom, | |
move: this.onMove as LeafletEventHandlerFn, | |
moveend: this.update, | |
zoomanim: this.onAnimZoom as LeafletEventHandlerFn, | |
}; | |
} | |
private updateTransform(center: L.LatLng, zoom: number) { | |
if (!this.map || !this.domContainer) return; | |
const scale = this.map.getZoomScale(zoom, this.zoom); | |
const viewHalf = this.map.getSize().multiplyBy(0.5 + this.options.padding); | |
const currentCenterPoint = this.map.project(this.center, zoom); | |
const topLeftOffset = viewHalf | |
.multiplyBy(-scale) | |
.add(currentCenterPoint) | |
.subtract(this.map._getNewPixelOrigin(center, zoom)); | |
if (L.Browser.any3d) { | |
L.DomUtil.setTransform(this.domContainer, topLeftOffset, scale); | |
} else { | |
L.DomUtil.setPosition(this.domContainer, topLeftOffset); | |
} | |
} | |
private onZoom = (_: LeafletEvent) => { | |
if (this.map) { | |
this.updateTransform(this.map.getCenter(), this.map.getZoom()); | |
} | |
}; | |
private onAnimZoom = (e: ZoomAnimEvent) => { | |
this.updateTransform(e.center, e.zoom); | |
}; | |
private onMove = (e: LeafletMouseEvent) => { | |
if (this.options.shouldRedrawOnMove(e)) { | |
this.update(e); | |
} | |
}; | |
private update = (e: Partial<LeafletEvent>) => { | |
if (!this.map || !this.renderer || !this.domContainer) { | |
return; | |
} | |
// Update pixel bounds of renderer container | |
const p = this.options.padding; | |
const mapSize = this.map.getSize(); | |
const min = this.map | |
.containerPointToLayerPoint(mapSize.multiplyBy(-p)) | |
.round(); | |
this.bounds = new L.Bounds( | |
min, | |
min.add(mapSize.multiplyBy(1 + p * 2)).round(), | |
); | |
this.center = this.map.getCenter(); | |
this.zoom = this.map.getZoom(); | |
if (this.doubleBuffering && this.auxRenderer) { | |
[this.renderer, this.auxRenderer] = [this.auxRenderer, this.renderer]; | |
} | |
const view = this.renderer.canvas; | |
const b = this.bounds; | |
const container = this.domContainer; | |
const size = b.getSize(); | |
const curWidth = parseInt(this.renderer.canvas.style.width, 10) || 0; | |
const curHeight = parseInt(this.renderer.canvas.style.height, 10) || 0; | |
if (curWidth !== size.x || curHeight !== size.y) { | |
if ("gl" in this.renderer) { | |
const gl = this.renderer.gl; | |
if (gl.drawingBufferWidth !== this.renderer.width) { | |
const resolution = gl.drawingBufferWidth / this.renderer.width; | |
this.renderer.resolution = resolution; | |
// if (this.renderer.rootRenderTarget) { | |
// this.renderer.rootRenderTarget.resolution = resolution; | |
// } | |
} | |
} | |
this.renderer.resize(size.x, size.y); | |
view.style.width = `${size.x.toString()}px`; | |
view.style.height = `${size.y.toString()}px`; | |
console.log(this.renderer.width, size); | |
} | |
const offset = b.min ?? new L.Point(0, 0); | |
if (this.doubleBuffering) { | |
// const self = this; | |
requestAnimationFrame(() => { | |
this.redraw(offset, e); | |
if (this.renderer && "gl" in this.renderer) { | |
this.renderer.gl.finish(); | |
} | |
view.style.visibility = "visible"; | |
if (this.auxRenderer) { | |
this.auxRenderer.canvas.style.visibility = "hidden"; | |
} | |
L.DomUtil.setPosition(container, offset); | |
}); | |
} else { | |
this.redraw(offset, e); | |
L.DomUtil.setPosition(container, offset); | |
} | |
}; | |
// private disableLeafletRounding() { | |
// L.Point.prototype._round = no_round; | |
// } | |
// private enableLeafletRounding() { | |
// L.Point.prototype._round = round; | |
// } | |
private redraw(offset: L.Point, e: Partial<LeafletEvent>) { | |
if (!this.map || !this.renderer) { | |
return; | |
} | |
// this.disableLeafletRounding(); | |
const { initialZoom, wgsOrigin, wgsInitialShift } = this.mapSetting; | |
const scale = this.map.getZoomScale(this.zoom, initialZoom); | |
const shift = this.map | |
.latLngToLayerPoint(wgsOrigin) | |
.subtract(wgsInitialShift.multiplyBy(scale)) | |
.subtract(offset); | |
this.container.scale.set(scale); | |
this.container.position.set(shift.x, shift.y); | |
this.drawCallback(this.renderer, e); | |
// this.enableLeafletRounding(); | |
} | |
private setEventSystem( | |
renderer: Renderer, | |
destroyInteractionManager: boolean, | |
autoPreventDefault: boolean, | |
) { | |
if (destroyInteractionManager) { | |
renderer.events.destroy(); | |
} else if (!autoPreventDefault) { | |
renderer.events.autoPreventDefault = false; | |
} | |
} | |
private async initializeRenderer() { | |
if (this.domContainer) { | |
return; | |
} | |
this.domContainer = L.DomUtil.create( | |
"div", | |
"leaflet-pixi-overlay leaflet-zoom-animated absolute", | |
); | |
this.getPane()?.appendChild(this.domContainer); | |
this.map?.getPanes().overlayPane.appendChild(this.domContainer); | |
this.renderer = await autoDetectRenderer(this.rendererOptions); | |
this.setEventSystem( | |
this.renderer, | |
this.options.destroyInteractionManager, | |
this.options.autoPreventDefault, | |
); | |
this.domContainer.appendChild(this.renderer.canvas); | |
if (this.options.doubleBuffering) { | |
this.auxRenderer = await autoDetectRenderer(this.rendererOptions); | |
this.setEventSystem( | |
this.auxRenderer, | |
this.options.destroyInteractionManager, | |
this.options.autoPreventDefault, | |
); | |
this.domContainer.appendChild(this.auxRenderer.canvas); | |
this.renderer.canvas.style.position = "absolute"; | |
this.auxRenderer.canvas.style.position = "absolute"; | |
} | |
} | |
} | |
export default PixiOverlay; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment