import * as T from '@turf/turf'
/**
* @description: leaflet内使用mapboxgl插件,copy from https://github.com/mapbox/mapbox-gl-leaflet
* 初始版本:0.0.16
* 改写后支持接入zondy mapboxgl内核库
* @param {*} L
* @param {*} mapboxgl
* @return {*}
*/
export default function leafletMapbox(L, mapboxgl) {
L.MapboxGL = L.Layer.extend({
options: {
updateInterval: 32,
// How much to extend the overlay view (relative to map size)
// e.g. 0.1 would be 10% of map view in each direction
// 内边距设置为1,在旋转时可以遮住当前屏幕范围
padding: 1,
// whether or not to register the mouse and keyboard
// events on the mapbox overlay
interactive: false,
// set the tilepane as the default pane to draw gl tiles
pane: 'tilePane',
// 裁剪区域
clippingArea: null,
// 级数偏移
zoomOffset: 0
},
initialize(options) {
L.setOptions(this, options)
if (options.accessToken) {
mapboxgl.accessToken = options.accessToken
}
// setup throttling the update event when panning
this._throttledUpdate = L.Util.throttle(
this._update,
this.options.updateInterval,
this
)
},
onAdd(map) {
if (!this._container) {
this._initContainer()
}
const paneName = this.getPaneName()
map.getPane(paneName).appendChild(this._container)
this._initGL()
this._offset = this._calcRotateOffset()
// work around https://github.com/mapbox/mapbox-gl-leaflet/issues/47
if (map.options.zoomAnimation) {
L.DomEvent.on(
map._proxy,
L.DomUtil.TRANSITION_END,
this._transitionEnd,
this
)
}
map._addZoomLimit(this)
},
onRemove(map) {
if (this._map._proxy && this._map.options.zoomAnimation) {
L.DomEvent.off(
this._map._proxy,
L.DomUtil.TRANSITION_END,
this._transitionEnd,
this
)
}
const paneName = this.getPaneName()
map.getPane(paneName).removeChild(this._container)
},
getEvents() {
return {
move: this._throttledUpdate, // sensibly throttle updating while panning
zoomanim: this._animateZoom, // applys the zoom animation to the <canvas>
zoom: this._pinchZoom, // animate every zoom event for smoother pinch-zooming
zoomstart: this._zoomStart, // flag starting a zoom to disable panning
zoomend: this._zoomEnd,
resize: this._resize
}
},
setClippingArea(data) {
this.options.clippingArea = data
this.clippingAreaBounds = null
this._update()
},
getClippingArea() {
return this.options.clippingArea
},
getMapboxMap() {
return this._glMap
},
getCanvas() {
return this._glMap.getCanvas()
},
getSize() {
return this._map.getSize().multiplyBy(1 + this.options.padding * 2)
},
getBounds() {
const halfSize = this.getSize().multiplyBy(0.5)
const center = this._map.latLngToContainerPoint(this._map.getCenter())
return L.latLngBounds(
this._map.containerPointToLatLng(center.subtract(halfSize)),
this._map.containerPointToLatLng(center.add(halfSize))
)
},
getContainer() {
return this._container
},
// returns the pane name set in options if it is a valid pane, defaults to tilePane
getPaneName() {
return this._map.getPane(this.options.pane)
? this.options.pane
: 'tilePane'
},
_initContainer() {
const container = (this._container = L.DomUtil.create(
'div',
'leaflet-gl-layer'
))
const size = this.getSize()
container.style.width = `${size.x}px`
container.style.height = `${size.y}px`
const topLeft = this._calcRotateOffset()
L.DomUtil.setPosition(container, topLeft)
},
_initGL() {
const center = this._map.getCenter()
const crs = this._map.options.crs
this._offsetZoom = 0
let projCRS
const crsOptions = crs.options
if (crsOptions) {
const mapboxTileSize = crsOptions.mapboxTileSize || 256
const projCRSOptions = {
resolutions: crsOptions.resolutions,
origin: crsOptions.origin,
tileSize: mapboxTileSize
}
if (mapboxgl.Proj && mapboxgl.Proj.CRS) {
projCRS = new mapboxgl.Proj.CRS(
crs.code,
crs.def || crs.code,
projCRSOptions
)
// projCRS getZoomOffset计算投影坐标系的偏移(针对超出投影系范围进行偏移处理)
// 计算当前使用的size,因为本质上是mapbox在渲染,并没有依赖于leaflet的网格图层,我们需要知道对应leaflet层级的瓦片是否正确,如果瓦片大小是512,需要偏移下mapbox的zoom层级
// Math.floor(mapboxTileSize/256 -1 )
this._offsetZoom =
projCRS.getZoomOffset() -
Math.floor(mapboxTileSize / 256 - 1) -
this.options.zoomOffset
} else {
if (crsOptions.bounds) {
const max = crsOptions.bounds.max
const min = crsOptions.bounds.min
projCRSOptions.bounds = [min.x, min.y, max.x, max.y]
}
projCRS = new mapboxgl.CRS(crs.code, projCRSOptions)
}
} else {
projCRS = crs.code
}
const options = L.extend({}, this.options, {
container: this._container,
center: [center.lng, center.lat],
zoom: this._map.getZoom() - 1,
attributionControl: false,
crs: projCRS,
minZoom: 0,
renderWorldCopies: false
})
if (!this._glMap) this._glMap = new mapboxgl.Map(options)
else {
this._glMap.setCenter(options.center)
this._glMap.setZoom(options.zoom)
}
// allow GL base map to pan beyond min/max latitudes
this._glMap.transform.latRange = null
this._transformGL(this._glMap)
if (this._glMap._canvas.canvas) {
// older versions of mapbox-gl surfaced the canvas differently
this._glMap._actualCanvas = this._glMap._canvas.canvas
} else {
this._glMap._actualCanvas = this._glMap._canvas
}
// treat child <canvas> element like L.ImageOverlay
const canvas = this._glMap._actualCanvas
L.DomUtil.addClass(canvas, 'leaflet-image-layer')
L.DomUtil.addClass(canvas, 'leaflet-zoom-animated')
if (this.options.interactive) {
L.DomUtil.addClass(canvas, 'leaflet-interactive')
}
if (this.options.className) {
L.DomUtil.addClass(canvas, this.options.className)
}
},
/**
* @description: 计算旋转实际的偏移值
* @return {*}
*/
_calcRotateOffset() {
// 需要注意的是,mapboxgl容器实际上是以中心点作为和leaflet覆盖层关联的点
// 也就是说,mapboxgl容器和leaflet容器中心点是一致的
// 旋转时如果还是根据左上角点计算,则在旋转后会出现偏移,因此以中心点作为偏移的基准点
const offset = this._map.getSize().multiplyBy(this.options.padding)
const viewHalf = this._map.getSize()._divideBy(2)
const topLeft = this._map
._latLngToNewLayerPoint(
this._map.getCenter(),
this._map.getZoom(),
this._map.getCenter()
)
.subtract(viewHalf)
.subtract(offset)
return topLeft
},
_update(e) {
if (!this._map) {
return
}
// update the offset so we can correct for it later when we zoom
this._offset = this._calcRotateOffset()
if (this._zooming) {
return
}
const gl = this._glMap
const size = this.getSize()
const container = this._container
this._transformGL(gl)
const x_round = Math.round(size.x)
const y_round = Math.round(size.y)
if (
Math.round(gl.transform.width) !== x_round ||
Math.round(gl.transform.height) !== y_round
) {
container.style.width = `${x_round}px`
container.style.height = `${y_round}px`
gl.transform.width = x_round
gl.transform.height = y_round
if (gl._resize !== null && gl._resize !== undefined) {
gl._resize()
} else {
gl.resize()
}
} else {
// older versions of mapbox-gl surfaced update publicly
if (gl._update !== null && gl._update !== undefined) {
gl._update()
} else {
gl.update()
}
}
},
_transformGL(gl) {
const center = this._clampCenter(this._map.getCenter())
// gl.setView([center.lat, center.lng], this._map.getZoom() - 1, 0);
// calling setView directly causes sync issues because it uses requestAnimFrame
const tr = gl.transform
tr.center = mapboxgl.LngLat.convert([center.lng, center.lat])
// 设置不进行自动重算zoom center
tr.resetZoomScale = false
// transfrom zoom等级为整数,否则会出现瓦片和实际位置偏移
// offsetZoom定义为mapboxgl内部层级偏移值,非0情况下表示,由于此投影坐标系超出投影系范围,故进行偏移
tr.zoom = Math.floor(this._map.getZoom() - this._offsetZoom - 1)
const currentCenter = tr.center
const centerOffset = [0, 0]
if (
Math.abs(currentCenter.lng - center.lng) > 10e-8 ||
Math.abs(currentCenter.lat - center.lat) > 10e-8
) {
const p1 = tr.coordinatePoint(tr.locationCoordinate(currentCenter))
const p2 = tr.coordinatePoint(tr.locationCoordinate(center))
centerOffset[0] = p1.x - p2.x
centerOffset[1] = p1.y - p2.y
}
// 偏移container 需要对照mapboxgl内核库偏移
const container = this._container
const topLeft = this._calcRotateOffset().add(centerOffset)
// 矢量瓦片裁剪处理程序
const polygon = this.options.clippingArea
if (polygon) {
// 计算裁剪区缓存四至
if (!this.clippingAreaBounds) {
const [x1, y1, x2, y2] = T.bbox(T.polygon(polygon))
this.clippingAreaBounds = L.latLngBounds(
L.latLng(y1, x1),
L.latLng(y2, x2)
)
}
// 先进行四至边界判断
if (this.clippingAreaBounds.intersects(this._map.getBounds())) {
const clipPxs = polygon.map((coords) => {
return coords.map((v) => {
const point = this._map.latLngToContainerPoint(
L.latLng(v[1], v[0])
)
return [point.x, point.y]
})
})
const polygon1 = T.polygon(clipPxs)
const size = this._map.getSize()
const polygon2 = T.polygon([
[
[0, 0],
[size.x, 0],
[size.x, size.y],
[0, size.y],
[0, 0]
]
])
const intersection = T.intersect(polygon1, polygon2)
if (intersection) {
const coords = T.coordAll(intersection)
if (coords && coords.length > 3) {
let clipStr = ''
coords.forEach((coord) => {
clipStr += `${coord[0]}px ${coord[1]}px,`
})
clipStr = clipStr.substring(0, clipStr.length - 1)
container.style.clipPath = `polygon(${clipStr})`
}
}
} else {
container.style.clipPath = 'none'
}
} else {
container.style.clipPath = 'none'
}
L.DomUtil.setPosition(container, topLeft)
},
_clampCenter(center) {
if (!center) return center
const lng = center.lng
const lat = Math.min(Math.max(center.lat, -90), 90)
return L.latLng(lat, lng)
},
// update the map constantly during a pinch zoom
_pinchZoom(e) {
this._glMap.jumpTo({
zoom: Math.floor(this._map.getZoom() - this._offsetZoom - 1),
center: this._clampCenter(this._map.getCenter())
})
},
// borrowed from L.ImageOverlay
// https://github.com/Leaflet/Leaflet/blob/master/src/layer/ImageOverlay.js#L139-L144
_animateZoom(e) {
const scale = this._map.getZoomScale(e.zoom)
const padding = this._map
.getSize()
.multiplyBy(this.options.padding * scale)
const viewHalf = this.getSize()._divideBy(2)
// corrections for padding (scaled), adapted from
// https://github.com/Leaflet/Leaflet/blob/master/src/map/Map.js#L1490-L1508
const topLeft = this._map
.project(e.center, e.zoom)
._subtract(viewHalf)
._add(this._map._getMapPanePos().add(padding))
._round()
const offset = this._map
.project(this._map.getBounds().getNorthWest(), e.zoom)
._subtract(topLeft)
L.DomUtil.setTransform(
this._glMap._actualCanvas,
offset.subtract(this._offset),
scale
)
},
_zoomStart(e) {
this._zooming = true
// 处理动画效果异常问题
if (this._container) {
this._container.style.visibility = 'hidden'
}
this._offset = this._calcRotateOffset()
},
_zoomEnd() {
const scale = this._map.getZoomScale(this._map.getZoom())
L.DomUtil.setTransform(
this._glMap._actualCanvas,
// https://github.com/mapbox/mapbox-gl-leaflet/pull/130
null,
scale
)
this._zooming = false
this._update()
// 处理动画效果异常问题
if (this._container) {
this._container.style.visibility = 'visible'
}
},
_transitionEnd(e) {
L.Util.requestAnimFrame(function () {
const zoom = this._map.getZoom()
const center = this._map.getCenter()
const offset = this._map.latLngToContainerPoint(
this._map.getBounds().getNorthWest()
)
// reset the scale and offset
L.DomUtil.setTransform(this._glMap._actualCanvas, offset, 1)
// enable panning once the gl map is ready again
this._glMap.once(
'moveend',
L.Util.bind(function () {
this._zoomEnd()
}, this)
)
// update the map position
this._glMap.jumpTo({
center: this._clampCenter(center),
zoom: Math.floor(zoom - this._offsetZoom - 1)
})
}, this)
},
_resize(e) {
this._transitionEnd(e)
}
})
L.mapboxGL = function (options) {
return new L.MapboxGL(options)
}
}