///
import Event from '../event'
import utils from './utils'
import BoundsItem from './BoundsItem'
import type { DistrictCluster } from './index'
/*declare global {
interface Window {
Loca: any
}
}*/
export interface StyleOption {
strokeColor?: string
strokeOpacity?: number
strokeWeight?: number
fillColor?: string
fillOpacity?: number
}
export interface FeatureStyleByLevelOption {
country?: StyleOption // 国家
province?: StyleOption // 省份
city?: StyleOption // 市
district?: StyleOption // 区县
}
export interface RenderOptions {
// engine?: 'default' | 'loca'
minHeightToShowSubFeatures?: number
minSiblingAvgHeightToShowSubFeatures?: number
minSubAvgHeightToShowSubFeatures?: number
featureStyleByLevel?: FeatureStyleByLevelOption
zIndex?: number // 层级
visible?: boolean // 是否显示
areaNodeCacheLimit?: number
getFeatureStyle?: (feature: any, dataItems: any[]) => StyleOption
zooms?: [number, number]
renderPolygon?: (feature: any, dataItems: any[]) => AMap.Polygon // 自定义绘制多边形
renderClusterMarker?: (feature: any, dataItems: any[]) => AMap.Marker // 自定义绘制标号
clusterMarkerEventSupport?: boolean // 聚合标注是否开启事件支持,默认true。
clusterMarkerClickToShowSub?: boolean // 点击聚合标注是否触发展示子级区划(即调用 zoomToShowSubFeatures 方法),默认true
featureEventSupport?: boolean // 区划面是否开启事件支持,默认true
featureClickToShowSub?: boolean // 点击区划面是否触发进入子级区划,默认false
}
interface CustomRenderOptions extends RenderOptions {
map: AMap.Map
}
type _OptOptions = Required
export class BaseRender extends Event {
baseId = 1
_ins: DistrictCluster
_currentZoom = 2
_currentScaleFactor?: number
_currentViewBounds?: BoundsItem
_currentViewBoundsInLngLat?: AMap.Bounds
_currentPixelRatio?: number
_currentFeatures: { feature: any; dataItems: any }[] = []
_currentRenderId?: number
_loadLeft = 0
_isRendering?: boolean
_opts: _OptOptions
_renderLaterId: any
_map: AMap.Map
_polygonCache: AMap.Polygon[] = []
_markerCache: AMap.Marker[] = []
layer: any
loca: any
_locaPolygonLayer: any
markerGroup?: AMap.OverlayGroup
constructor(districtCluster: DistrictCluster, options: CustomRenderOptions) {
super()
this._opts = utils.extend(
{
engine: 'default',
areaNodeCacheLimit: -1,
minHeightToShowSubFeatures: 630,
minSiblingAvgHeightToShowSubFeatures: 600,
minSubAvgHeightToShowSubFeatures: 300,
zooms: [2, 30],
clusterMarkerEventSupport: true, // 聚合标注是否开启事件支持,默认true。
clusterMarkerClickToShowSub: true, // 点击聚合标注是否触发展示子级区划(即调用 zoomToShowSubFeatures 方法),默认true
featureEventSupport: true, // 区划面是否开启事件支持,默认true
featureClickToShowSub: false,
featureStyleByLevel: {
country: {
strokeColor: 'rgb(31, 119, 180)',
strokeOpacity: 0.9,
strokeWeight: 2,
fillColor: 'rgb(49, 163, 84)',
fillOpacity: 0.8
},
province: {
strokeColor: 'rgb(31, 119, 180)',
strokeOpacity: 0.9,
strokeWeight: 2,
fillColor: 'rgb(116, 196, 118)',
fillOpacity: 0.7
},
city: {
strokeColor: 'rgb(31, 119, 180)',
strokeOpacity: 0.9,
strokeWeight: 2,
fillColor: 'rgb(161, 217, 155)',
fillOpacity: 0.6
},
district: {
strokeColor: 'rgb(31, 119, 180)',
strokeOpacity: 0.9,
strokeWeight: 2,
fillColor: 'rgb(199, 233, 192)',
fillOpacity: 0.5
}
}
},
options
)
this._map = this._opts.map
this._createLayer()
this._ins = districtCluster
this._isRendering = !1
this._loadLeft = 0
this._currentFeatures = []
}
_createLayer() {
this.markerGroup = new AMap.OverlayGroup()
this._map.add(this.markerGroup as any)
/*if(this._opts.engine === 'loca'){
this.loca = new window.Loca.Container({
map: this._map
});
this._locaPolygonLayer = new window.Loca.PolygonLayer({
zIndex: this._opts.zIndex,
visible: this._opts.visible
})
this._locaPolygonLayer.setSource(new window.Loca.GeoJSONSource({
data: {
"type": "FeatureCollection",
"features": []
}
}))
this._locaPolygonLayer.setStyle({
topColor: (index, feature) => {
return this._getFeatureStyleOptions(feature, feature.properties.dataItems).fillColor
}
})
this.loca.add(this._locaPolygonLayer)
}else{*/
this.layer = new (AMap as any).VectorLayer({
zIndex: this._opts.zIndex || 10,
visible: this._opts.visible || true
})
this._map.addLayer(this.layer)
// }
}
zoomToShowSubFeatures(adcode, center?: any) {
const minZoomToShowSub = this.getMinZoomToShowSub(adcode)
if (minZoomToShowSub >= 3) {
const map = this._ins.getMap()
if (map) {
if (!center) {
const treeNode = this._ins._distMgr.getNodeByAdcode(adcode)
center = treeNode.center
}
map.setZoomAndCenter(minZoomToShowSub, center)
}
}
}
getPixelRatio() {
return Math.min(2, Math.round(window.devicePixelRatio || 1))
}
refreshViewState() {
if (!this._ins._distMgr.isReady()) return !1
const simpIns = this._ins
if (!simpIns.isReady()) return !1
const map = simpIns.getMap() as AMap.Map,
mapViewBounds = map.getBounds(),
viewSize = map.getSize(),
currZoom = map.getZoom(3),
maxZoom = this._opts.zooms[1],
scaleFactor = Math.pow(2, maxZoom - currZoom),
northWest = mapViewBounds.getNorthWest(),
topLeft = map.lngLatToCoords([northWest.getLng(), northWest.getLat()]),
bounds = new BoundsItem(topLeft[0], topLeft[1], viewSize.width * scaleFactor, viewSize.height * scaleFactor)
this._currentZoom = currZoom
this._currentScaleFactor = scaleFactor
this._currentViewBounds = bounds
this._currentViewBoundsInLngLat = mapViewBounds
this._currentPixelRatio = this.getPixelRatio()
}
renderViewport() {
this.refreshViewState()
if (!this._currentViewBounds) return !1
// this.markerGroup.clearOverlays()
// this.layer.clear()
// this._polygonCache = []
// this._markerCache = []
this._currentRenderId = this.baseId++
this._loadLeft = 0
this._currentFeatures = []
this._renderViewDist(this._currentRenderId)
this._isRendering = false
}
getCurrentRenderId() {
return this._currentRenderId
}
isRenderIdStillValid(renderId) {
return renderId === this._currentRenderId
}
_renderViewDist(renderId) {
const adcodes2Render: any[] = []
if (this._currentZoom < this._opts.zooms[0] || this._currentZoom > this._opts.zooms[1]) {
this.isRenderIdStillValid(renderId) && this._prepareFeatures(renderId, adcodes2Render)
return
}
this._ins.getDistMgr().traverseTopNodes(
this._currentViewBoundsInLngLat as AMap.Bounds,
this._currentZoom,
(node) => {
adcodes2Render.push(node.adcode)
},
() => {
// console.log('adcodes2Render: ', adcodes2Render)
this.isRenderIdStillValid(renderId) && this._prepareFeatures(renderId, adcodes2Render)
},
this
)
}
getMinZoomToShowSub(adcode) {
const treeNode = this._ins._distMgr.getNodeByAdcode(adcode)
if (!treeNode || !treeNode.idealZoom) return -1
if (!treeNode._minZoomToShowSub) {
const zooms = this._ins.getZooms() as [number, number]
for (let i = zooms[0]; i <= zooms[1]; i++)
if (this.shouldShowSubOnZoom(treeNode, i)) {
treeNode._minZoomToShowSub = i
break
}
}
return treeNode._minZoomToShowSub || -1
}
shouldShowSubOnZoom(treeNode, zoom) {
if (!treeNode.idealZoom) return !1
if (treeNode._minZoomToShowSub && zoom >= treeNode._minZoomToShowSub) return !0
let boundsSize = this._ins._distMgr.getNodeBoundsSize(treeNode, zoom)
if (1e5 === treeNode.adcode && boundsSize[1] > 400) return !0
if (boundsSize[1] < this._opts.minHeightToShowSubFeatures) return !1
let i, len, heightSum
if (treeNode.children) {
const children = treeNode.children
heightSum = 0
len = children.length
if (len) {
for (i = 0; i < len; i++) {
boundsSize = this._ins._distMgr.getNodeBoundsSize(children[i], zoom)
heightSum += boundsSize[1]
}
if (heightSum / len < this._opts.minSubAvgHeightToShowSubFeatures) return !1
}
}
const parentAdcode = this._ins._distMgr.getParentAdcode(treeNode.adcode, treeNode.acroutes)
if (parentAdcode) {
const parentNode = this._ins._distMgr.getNodeByAdcode(parentAdcode),
siblings = parentNode.children
siblings || console.error('No children bound', treeNode, parentNode)
len = siblings.length
if (len > 1) {
heightSum = 0
for (i = 0; i < len; i++)
if (siblings[i].adcode !== treeNode.adcode) {
boundsSize = this._ins._distMgr.getNodeBoundsSize(siblings[i], zoom)
heightSum += boundsSize[1]
}
if (heightSum / (len - 1) < this._opts.minSiblingAvgHeightToShowSubFeatures) return !1
}
}
return !0
}
_shouldShowSub(treeNode) {
return !(!treeNode.children || !treeNode.children.length) && this.shouldShowSubOnZoom(treeNode, this._currentZoom)
}
_prepareFeatures(renderId, adcodes) {
const justSelfList: any[] = [],
showSubList: any[] = []
for (let i = 0, len = adcodes.length; i < len; i++) {
const treeNode = this._ins._distMgr.getNodeByAdcode(adcodes[i])
if (!treeNode) throw new Error(`Can not find node: ${adcodes[i]}`)
this._shouldShowSub(treeNode) ? showSubList.push(adcodes[i]) : justSelfList.push(adcodes[i])
}
this._prepareSelfFeatures(renderId, justSelfList)
this._prepareSubFeatures(renderId, showSubList)
this._checkLoadFinish(renderId)
}
_prepareSelfFeatures(renderId, adcodes) {
let toLoadAdcode
const currZoom = this._currentZoom as number
for (let i = 0, len = adcodes.length; i < len; i++) {
const treeNode = this._ins._distMgr.getNodeByAdcode(adcodes[i])
toLoadAdcode = null
if (treeNode.acroutes) {
const parentNode = this._ins._distMgr.getNodeByAdcode(treeNode.acroutes[treeNode.acroutes.length - 1])
;(!treeNode.idealZoom ||
currZoom < treeNode.idealZoom - 1 ||
Math.abs(currZoom - parentNode.idealZoom) <= Math.abs(treeNode.idealZoom - currZoom)) &&
(toLoadAdcode = parentNode.adcode)
}
this._loadAndRenderSelf(renderId, toLoadAdcode ? toLoadAdcode : adcodes[i], adcodes[i])
}
}
_prepareSubFeatures(renderId, adcodes) {
let i, len
for (i = 0, len = adcodes.length; i < len; i++) {
this._loadAndRenderSub(renderId, adcodes[i])
}
}
_renderSelf(renderId, adcode, areaNode) {
let feature
if (adcode === areaNode.getAdcode()) {
feature = areaNode.getParentFeature()
} else {
const subFeatures = areaNode.getSubFeatures(),
subIdx = this._ins._distMgr.getSubIdx(adcode)
feature = subFeatures[subIdx]
if (!feature) {
console.warn('Werid, can not find sub feature', areaNode.getAdcode(), adcode)
return
}
if (feature.properties.adcode !== adcode) {
console.warn('Sub adcode not match!!', subFeatures, subIdx)
return
}
}
this._ins.getDistCounter().calcDistGroup(
adcode,
!1,
() => {
this.isRenderIdStillValid(renderId) && this._prepRenderFeatureInPixel(renderId, feature)
},
this
)
}
_checkLoadFinish(renderId) {
if (0 === this._loadLeft) {
const self = this
setTimeout(function () {
self.isRenderIdStillValid(renderId) && self._handleRenderFinish()
}, 0)
}
}
_renderSub(renderId, areaNode) {
const subFeatures = areaNode.getSubFeatures()
this._ins.getDistCounter().calcDistGroup(
areaNode.getAdcode(),
!0,
() => {
if (this.isRenderIdStillValid(renderId))
for (let i = 0, len = subFeatures.length; i < len; i++)
this._prepRenderFeatureInPixel(renderId, subFeatures[i])
},
this
)
}
_handleRenderFinish() {
this._tryFreeMemery()
this._renderAllFeature()
}
_renderAllFeature() {
// if (this._opts.engine === 'loca') {
// this._renderAllFeatureByLoca()
// } else {
this._renderAllFeatureByDefault()
// }
}
/*_renderAllFeatureByLoca() {
const featureData = {
type: 'FeatureCollection',
features: []
} as any
this._currentFeatures.forEach((item) => {
const feature = item.feature
feature.properties.dataItems = item.dataItems
featureData.features.push(feature)
})
this._locaPolygonLayer.setSource(
new window.Loca.GeoJSONSource({
data: featureData
})
)
}*/
_renderAllFeatureByDefault() {
// console.log('this._currentFeatures: ', this._currentFeatures)
// 存储需要新增的面区划
const needRenderPolygons: AMap.Polygon[] = []
// 存储需要移除的面区划
const needRemovePolygon: AMap.Polygon[] = []
// 存储需要新增的聚合点
const needRenderMarker: AMap.Marker[] = []
// 存储需要移除的聚合点
const needRemoveMarker: AMap.Marker[] = []
for (let i = 0; i < this._polygonCache.length; i++) {
const prePolygon = this._polygonCache[i]
const adcode = prePolygon.getExtData()._data.adcode
let isInCache = false
for (let j = 0; j < this._currentFeatures.length; j++) {
const item = this._currentFeatures[j]
const feature = item.feature
const props = feature.properties
if (adcode === props.adcode) {
isInCache = true
this._currentFeatures.splice(j, 1)
break
}
}
if (!isInCache) {
needRemovePolygon.push(prePolygon)
this._polygonCache.splice(i, 1)
needRemoveMarker.push(this._markerCache[i])
this._markerCache.splice(i, 1)
i--
}
}
this._currentFeatures.forEach((item) => {
const polygon = this._createPolygonFeature(item.feature, item.dataItems)
if (this._opts.featureEventSupport) {
polygon.on(
'click',
utils.bind((e) => {
this.emit('featureClick', e, item.feature)
if (this._opts.featureClickToShowSub) {
this._ins.zoomToShowSubFeatures(item.feature.properties.adcode)
}
}, this)
)
polygon.on(
'mouseover',
utils.bind((e) => {
this.emit('featureMouseover', e, item.feature)
}, this)
)
polygon.on(
'mouseout',
utils.bind((e) => {
this.emit('featureMouseout', e, item.feature)
}, this)
)
}
const marker = this._createClusterMarker(item.feature, item.dataItems)
if (this._opts.clusterMarkerEventSupport) {
marker.on(
'click',
utils.bind((e) => {
this.emit('clusterMarkerClick', e, {
adcode: item.feature.properties.adcode,
...item
})
if (this._opts.clusterMarkerClickToShowSub) {
this._ins.zoomToShowSubFeatures(item.feature.properties.adcode)
}
}, this)
)
}
needRenderPolygons.push(polygon)
needRenderMarker.push(marker)
})
this.layer.remove(needRemovePolygon)
this.markerGroup?.removeOverlays(needRemoveMarker)
this.layer.add(needRenderPolygons)
this._polygonCache.push(...needRenderPolygons)
needRenderPolygons.length = 0
this.markerGroup?.addOverlays(needRenderMarker)
this._markerCache.push(...needRenderMarker)
needRenderMarker.length = 0
}
_tryFreeMemery() {
this._ins.getDistMgr().tryClearCache(this._currentRenderId, this._opts.areaNodeCacheLimit)
}
_increaseLoadLeft() {
this._loadLeft++
}
_decreaseLoadLeft(renderId) {
this._loadLeft--
0 === this._loadLeft && this._checkLoadFinish(renderId)
}
_loadAndRenderSelf(renderId, loadAdcode, adcode) {
this._ins.getDistMgr().touchAdcode(loadAdcode, renderId)
const distExplorer = this._ins._distMgr.getExplorer(),
areaNode = distExplorer.getLocalAreaNode(loadAdcode)
if (areaNode) this._renderSelf(renderId, adcode, areaNode)
else {
this._increaseLoadLeft()
distExplorer.loadAreaNode(
loadAdcode,
(error, areaNode) => {
if (this.isRenderIdStillValid(renderId)) {
error ? console.error(error) : this._renderSelf(renderId, adcode, areaNode)
this._decreaseLoadLeft(renderId)
}
},
this
)
}
}
_loadAndRenderSub(renderId, adcode) {
this._ins.getDistMgr().touchAdcode(adcode, renderId)
const distExplorer = this._ins._distMgr.getExplorer(),
areaNode = distExplorer.getLocalAreaNode(adcode)
if (areaNode) {
this._renderSub(renderId, areaNode)
} else {
this._increaseLoadLeft()
distExplorer.loadAreaNode(
adcode,
(error, areaNode) => {
if (this.isRenderIdStillValid(renderId)) {
error ? console.error(error) : this._renderSub(renderId, areaNode)
this._decreaseLoadLeft(renderId)
}
},
this
)
}
}
_prepRenderFeatureInPixel(renderId, feature) {
if (!this._ins.getDistMgr().isExcludedAdcode(feature.properties.adcode)) {
const dataItems = this._ins.getDistCounter().getPackItemsByAdcode(feature.properties.adcode)
this._currentFeatures.push({
feature,
dataItems
})
// this.renderClusterMarker(renderId, feature, dataItems)
// this.renderFeature(renderId, feature, null, dataItems)
}
}
_createPolygonFeature(feature, dataItems) {
const props = Object.assign({}, feature.properties)
props.dataItems = dataItems
if (this._opts.renderPolygon) {
const polygon = this._opts.renderPolygon(feature, dataItems)
const extData = polygon.getExtData() || {}
extData._data = props
polygon.setExtData(extData)
return polygon
}
const styleOptions = this._getFeatureStyleOptions(feature, dataItems) || {}
return new (AMap as any).Polygon({
path: feature.geometry.coordinates,
...styleOptions,
extData: {
_data: props
}
})
}
_createClusterMarker(feature, dataItems) {
const props = feature.properties
props.dataItems = dataItems
if (this._opts.renderClusterMarker) {
const marker = this._opts.renderClusterMarker(feature, dataItems)
const extData = marker.getExtData() || {}
extData._data = props
marker.setExtData(extData)
return marker
}
const nodeClassNames = {
title: 'amap-ui-district-cluster-marker-title',
body: 'amap-ui-district-cluster-marker-body',
container: 'amap-ui-district-cluster-marker'
}
const container = document.createElement('div')
const title = document.createElement('span')
title.className = nodeClassNames.title
const body = document.createElement('span')
body.className = nodeClassNames.body
container.appendChild(title)
container.appendChild(body)
const routeNames: any[] = [],
classNameList = [nodeClassNames.container, `level_${props.level}`, `adcode_${props.adcode}`]
if (props.acroutes)
for (let acroutes = props.acroutes, i = 0, len = acroutes.length; i < len; i++) {
classNameList.push(`descendant_of_${acroutes[i]}`)
i === len - 1 && classNameList.push(`child_of_${acroutes[i]}`)
i > 0 && routeNames.push(this._ins._distMgr.getNodeByAdcode(acroutes[i]).name)
}
container.className = classNameList.join(' ')
if (routeNames.length > 0) {
routeNames.push(props.name)
container.setAttribute('title', routeNames.join('>'))
} else container.removeAttribute('title')
title.innerHTML = utils.escapeHtml(props.name)
body.innerHTML = dataItems.length
return new AMap.Marker({
topWhenClick: !0,
offset: new AMap.Pixel(-20, -30),
content: container,
position: props.center,
extData: {
_data: props
}
})
}
_getFeatureStyleOptions(feature, dataItems): StyleOption {
const styleGetter = this._opts.getFeatureStyle
const defaultStyle = this._opts.featureStyleByLevel[feature.properties.level]
if (!styleGetter) {
return defaultStyle
}
const styleOptions = styleGetter.call(null, feature, dataItems)
return !styleOptions
? defaultStyle
: utils.extend({}, this._opts.featureStyleByLevel[feature.properties.level], styleOptions)
}
renderLater(delay?: number) {
if (!this._renderLaterId) {
this._renderLaterId = setTimeout(() => {
this.render()
}, delay || 100)
}
}
isRendering() {
return this._isRendering
}
render() {
if (this._renderLaterId) {
clearTimeout(this._renderLaterId)
this._renderLaterId = null
}
this._isRendering = true
this._ins._distMgr.onReady(this.renderViewport, this, !0)
}
forceRender() {
if (this._renderLaterId) {
clearTimeout(this._renderLaterId)
this._renderLaterId = null
}
this._isRendering = true
this.clear()
this._ins._distMgr.onReady(this.renderViewport, this, !0)
}
getOption(k) {
return this._opts[k]
}
getOptions() {
return this._opts
}
show() {
this.layer.show()
this.markerGroup?.show()
}
hide() {
this.layer.hide()
this.markerGroup?.hide()
}
clear() {
this.layer.clear()
this.markerGroup?.clearOverlays()
this._polygonCache = []
this._markerCache = []
}
setzIndex(zIndex: number) {
this.layer.setzIndex(zIndex)
}
getZooms() {
return this._opts.zooms
}
destroy() {
this._map.removeLayer(this.layer)
this._map.remove(this.markerGroup as any)
this._currentFeatures = []
this.clear()
this.layer = null
this._map = null as any
this._ins = null as any
}
}