From 05f6815d00207d09375b6d0d629339ae83203539 Mon Sep 17 00:00:00 2001 From: PureBlack <52806702+Pureblackkk@users.noreply.github.com> Date: Sat, 28 Oct 2023 12:36:38 +0800 Subject: [PATCH] feat(packages/maps): support tencent map (#1966) * feat(packages/maps): support tencent map * fix: upgrade tencent types --------- Co-authored-by: @thinkinggis --- package.json | 1 + packages/maps/src/index.ts | 2 + packages/maps/src/tmap/index.ts | 9 + packages/maps/src/tmap/logo.css | 3 + packages/maps/src/tmap/map.ts | 468 ++++++++++++++++++ packages/maps/src/tmap/maploader.ts | 113 +++++ packages/maps/typings/index.d.ts | 1 + .../site/examples/tutorial/map/demo/meta.json | 11 + .../examples/tutorial/map/demo/tencentmap.js | 11 + .../tutorial/map/demo/tmapInstance.js | 48 ++ 10 files changed, 667 insertions(+) create mode 100644 packages/maps/src/tmap/index.ts create mode 100644 packages/maps/src/tmap/logo.css create mode 100644 packages/maps/src/tmap/map.ts create mode 100644 packages/maps/src/tmap/maploader.ts create mode 100644 packages/site/examples/tutorial/map/demo/tencentmap.js create mode 100644 packages/site/examples/tutorial/map/demo/tmapInstance.js diff --git a/package.json b/package.json index 8af42a99a5..2831f1cad7 100644 --- a/package.json +++ b/package.json @@ -215,6 +215,7 @@ "stylelint-config-styled-components": "^0.1.1", "stylelint-processor-styled-components": "^1.3.2", "svg-inline-loader": "^0.8.0", + "tmap-types-temporary": "0.1.4", "tokml": "^0.4.0", "topojson": "^3.0.2", "ts-jest": "^24.0.2", diff --git a/packages/maps/src/index.ts b/packages/maps/src/index.ts index 053577a728..6762155154 100644 --- a/packages/maps/src/index.ts +++ b/packages/maps/src/index.ts @@ -4,6 +4,7 @@ import BaiduMap from './bmap/'; import Earth from './earth/'; import Map from './map/'; import Mapbox from './mapbox/'; +import TencentMap from './tmap'; import { Version } from './version'; export * from './utils'; export { @@ -15,4 +16,5 @@ export { Map, Earth, BaiduMap, + TencentMap, }; diff --git a/packages/maps/src/tmap/index.ts b/packages/maps/src/tmap/index.ts new file mode 100644 index 0000000000..fb3907b78b --- /dev/null +++ b/packages/maps/src/tmap/index.ts @@ -0,0 +1,9 @@ +/* eslint-disable */ +import BaseMapWrapper from '../utils/BaseMapWrapper'; +import TMapService from './map'; + +export default class TMapWrapper extends BaseMapWrapper { + protected getServiceConstructor() { + return TMapService; + } +} diff --git a/packages/maps/src/tmap/logo.css b/packages/maps/src/tmap/logo.css new file mode 100644 index 0000000000..251194a259 --- /dev/null +++ b/packages/maps/src/tmap/logo.css @@ -0,0 +1,3 @@ +.tmap-contianer--hide-logo img[src*='mapapi.qq.com/web/jsapi/logo/logo_def.png'] { + display: none; +} diff --git a/packages/maps/src/tmap/map.ts b/packages/maps/src/tmap/map.ts new file mode 100644 index 0000000000..2952750f40 --- /dev/null +++ b/packages/maps/src/tmap/map.ts @@ -0,0 +1,468 @@ +import { + Bounds, + ICameraOptions, + ILngLat, + IMercator, + IPoint, + IStatusOptions, + IViewport, + MapServiceEvent, + MapStyleConfig, + Point, +} from '@antv/l7-core'; +import { MercatorCoordinate } from '@antv/l7-map'; +import { DOM } from '@antv/l7-utils'; +import { mat4, vec3 } from 'gl-matrix'; +import BaseMapService from '../utils/BaseMapService'; +import Viewport from '../utils/Viewport'; +import './logo.css'; +import TMapLoader from './maploader'; + +const TMAP_API_KEY: string = 'OB4BZ-D4W3U-B7VVO-4PJWW-6TKDJ-WPB77'; +const BMAP_VERSION: string = '1.exp'; + +const EventMap: { + [key: string]: any; +} = { + mapmove: 'center_changed', + camerachange: ['drag', 'pan', 'rotate', 'pitch', 'zoom'], + zoomchange: 'zoom', + dragging: 'drag', +}; + +export default class TMapService extends BaseMapService { + // @ts-ignore + protected viewport: IViewport = null; + + public handleCameraChanged = (e?: any) => { + // Trigger map change event + this.emit('mapchange'); + // resync + const map = this.map; + // @ts-ignore + const { lng, lat } = map.getCenter(); + const option = { + center: [lng, lat], + // @ts-ignore + viewportHeight: map.getContainer().clientHeight, + // @ts-ignore + viewportWidth: map.getContainer().clientWidth, + // @ts-ignore + bearing: map.getHeading(), + // @ts-ignore + pitch: map.getPitch(), + // @ts-ignore + zoom: map.getZoom() - 1, + }; + + this.viewport.syncWithMapCamera(option as any); + this.updateCoordinateSystemService(); + this.cameraChangedCallback(this.viewport); + }; + + public async init(): Promise { + this.viewport = new Viewport(); + + // TODO: Handle initial config + const { + id, + mapInstance, + center = [121.30654632240122, 31.25744185633306], + token = TMAP_API_KEY, + version = BMAP_VERSION, + libraries = [], + minZoom = 3, + maxZoom = 18, + rotation = 0, + pitch = 0, + mapSize = 10000, + logoVisible = true, + ...rest + } = this.config; + + if (!(window.TMap || mapInstance)) { + await TMapLoader.load({ + key: token, + version, + libraries, + }); + } + + if (mapInstance) { + // If there's already a map instance, maybe not setting any other configurations + this.map = mapInstance as any; + this.$mapContainer = this.map.getContainer(); + if (logoVisible === false) { + this.hideLogo(); + } + } else { + if (!id) { + throw Error('No container id specified'); + } + const mapContainer = DOM.getContainer(id)!; + + const map = new TMap.Map(mapContainer, { + maxZoom, + minZoom, + rotation, + pitch, + showControl: false, + // Tencent use (Lat, Lng) while center is (Lng, Lat) + center: new TMap.LatLng(center[1], center[0]), + ...rest, + }); + + // @ts-ignore + this.map = map; + // @ts-ignore + this.$mapContainer = map.getContainer(); + if (logoVisible === false) { + this.hideLogo(); + } + } + + // Set tencent map canvas element position as absolute + // @ts-ignore + this.map.canvasContainer.style.position = 'absolute'; + this.simpleMapCoord.setSize(mapSize); + + // May be find an integrated event replacing following events + this.map.on('drag', this.handleCameraChanged); + this.map.on('pan', this.handleCameraChanged); + this.map.on('rotate', this.handleCameraChanged); + this.map.on('pitch', this.handleCameraChanged); + this.map.on('zoom', this.handleCameraChanged); + + // Trigger camera change after init + this.handleCameraChanged(); + } + + public destroy(): void { + this.map.destroy(); + } + + public onCameraChanged(callback: (viewport: IViewport) => void): void { + this.cameraChangedCallback = callback; + } + + public addMarkerContainer(): void { + const container = this.map.getContainer(); + this.markerContainer = DOM.create('div', 'l7-marker-container', container); + this.markerContainer.setAttribute('tabindex', '-1'); + } + + public getMarkerContainer(): HTMLElement { + return this.markerContainer; + } + + // MapEvent + public on(type: string, handle: (...args: any[]) => void): void { + if (MapServiceEvent.indexOf(type) !== -1) { + this.eventEmitter.on(type, handle); + } else { + if (Array.isArray(EventMap[type])) { + EventMap[type].forEach((eventName: string) => { + this.map.on(eventName || type, handle); + }); + } else { + this.map.on(EventMap[type] || type, handle); + } + } + } + + public off(type: string, handle: (...args: any[]) => void): void { + this.map.off(EventMap[type] || type, handle); + this.eventEmitter.off(type, handle); + } + + public once(type: string, handler: (...args: any[]) => void): void { + throw new Error('Method not implemented.'); + } + + // get dom + public getContainer(): HTMLElement | null { + return this.map.getContainer(); + } + + public getSize(): [number, number] { + // @ts-ignore + return [this.map.width, this.map.height]; + } + + // get map status method + public getMinZoom(): number { + // @ts-ignore + return this.map.transform._minZoom; + } + + public getMaxZoom(): number { + // @ts-ignore + return this.map.transform._maxZoom; + } + + // get map params + public getType() { + return 'tmap'; + } + + public getZoom(): number { + return this.map.getZoom(); + } + public getCenter(): ILngLat { + const { lng, lat } = this.map.getCenter(); + return { + lng, + lat, + }; + } + public getPitch(): number { + return this.map.getPitch(); + } + + public getRotation(): number { + return this.map.getRotation(); + } + + public getBounds(): Bounds { + const ne = this.map.getBounds().getNorthEast(); + const sw = this.map.getBounds().getSouthWest(); + return [ + [sw.lng, sw.lat], + [ne.lng, ne.lat], + ]; + } + + public getMapContainer(): HTMLElement { + return this.map.getContainer(); + } + + public getMapCanvasContainer(): HTMLElement { + return this.map.getContainer()?.getElementsByTagName('canvas')[0]; + } + + public getMapStyleConfig(): MapStyleConfig { + // return this.getMap() + throw new Error('Method not implemented.'); + } + + public setBgColor(color: string): void { + this.bgColor = color; + } + + public setMapStyle(styleId: any): void { + this.map.setMapStyleId(styleId); + } + + // control with raw map + public setRotation(rotation: number): void { + this.map.setRotation(rotation); + } + + public zoomIn(): void { + this.map.setZoom(this.getZoom() + 1); + } + + public zoomOut(option?: any, eventData?: any): void { + this.map.setZoom(this.getZoom() - 1); + } + + public panTo([lng, lat]: Point): void { + this.map.panTo(new TMap.LatLng(lat, lng)); + } + + public panBy(x: number, y: number): void { + this.map.panBy([x, y]); + } + + public fitBounds(bound: Bounds, fitBoundsOptions?: unknown): void { + const [sw, ne] = bound; + const swLatLng = new TMap.LatLng(sw[1], sw[0]); + const neLatLng = new TMap.LatLng(ne[1], ne[0]); + const bounds = new TMap.LatLngBounds(swLatLng, neLatLng); + // @ts-ignore + this.map.fitBounds(bounds, fitBoundsOptions); + } + + public setZoomAndCenter(zoom: number, [lng, lat]: Point): void { + this.map.setCenter(new TMap.LatLng(lat, lng)); + this.map.setZoom(zoom); + } + + public setCenter( + [lng, lat]: [number, number], + option?: ICameraOptions, + ): void { + this.map.setCenter(new TMap.LatLng(lat, lng)); + } + + public setPitch(pitch: number): any { + this.map.setPitch(pitch); + } + + public setZoom(zoom: number): any { + this.map.setZoom(zoom); + } + + public setMapStatus(option: Partial): void { + (Object.keys(option) as Array).map((status) => { + switch (status) { + case 'doubleClickZoom': + this.map.setDoubleClickZoom(!!option.doubleClickZoom); + break; + case 'dragEnable': + this.map.setDraggable(!!option.doubleClickZoom); + break; + case 'rotateEnable': + // @ts-ignore + this.map.setRotatable(!!option.rotateEnable); + break; + case 'zoomEnable': + this.map.setDoubleClickZoom(!!option.zoomEnable); + this.map.setScrollable(!!option.zoomEnable); + break; + case 'keyboardEnable': + case 'resizeEnable': + case 'showIndoorMap': + throw Error('Options may bot be supported'); + default: + } + }); + } + + // coordinates methods + public meterToCoord( + [centerLon, centerLat]: [number, number], + [outerLon, outerLat]: [number, number], + ) { + const metreDistance = TMap.geometry.computeDistance([ + new TMap.LatLng(centerLat, centerLon), + new TMap.LatLng(outerLat, outerLon), + ]); + + const [x1, y1] = this.lngLatToCoord!([centerLon, centerLat]); + const [x2, y2] = this.lngLatToCoord!([outerLon, outerLat]); + const coordDistance = Math.sqrt( + Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2), + ); + + return coordDistance / metreDistance; + } + + public pixelToLngLat([x, y]: Point): ILngLat { + // Since tecent map didn't provide the reverse method for transforming from pixel to lnglat + // It had to be done by calculate the relative distance in container coordinates + const { lng: clng, lat: clat } = this.map.getCenter(); + const { x: centerPixelX, y: centerPixelY } = this.lngLatToPixel([ + clng, + clat, + ]); + const { x: centerContainerX, y: centerContainerY } = this.lngLatToContainer( + [clng, clat], + ); + const { lng, lat } = this.map.unprojectFromContainer( + new TMap.Point( + centerContainerX + (x - centerPixelX), + centerContainerY + (y - centerPixelY), + ), + ); + return this.containerToLngLat([lng, lat]); + } + + public lngLatToPixel([lng, lat]: Point): IPoint { + // @ts-ignore + const { x, y } = this.map.projectToWorldPlane(new TMap.LatLng(lat, lng)); + return { x, y }; + } + + public containerToLngLat([x, y]: [number, number]): ILngLat { + const { lng, lat } = this.map.unprojectFromContainer(new TMap.Point(x, y)); + return { lng, lat }; + } + + public lngLatToContainer([lng, lat]: [number, number]): IPoint { + // @ts-ignore + const { x, y } = this.map.projectToContainer(new TMap.LatLng(lat, lng)); + return { x, y }; + } + + public lngLatToCoord?([lng, lat]: [number, number]): [number, number] { + // TODO: Perhaps need to check the three.js coordinates + const { x, y } = this.lngLatToPixel([lng, lat]); + return [x, -y]; + } + + public lngLatToCoords?(list: number[][] | number[][][]): any { + return list.map((item) => + Array.isArray(item[0]) + ? this.lngLatToCoords!(item as Array<[number, number]>) + : this.lngLatToCoord!(item as [number, number]), + ); + } + + public lngLatToMercator( + lnglat: [number, number], + altitude: number, + ): IMercator { + // Use built in mercator tools due to Tencent not provided related methods + const { + x = 0, + y = 0, + z = 0, + } = MercatorCoordinate.fromLngLat(lnglat, altitude); + return { x, y, z }; + } + + public getModelMatrix( + lnglat: [number, number], + altitude: number, + rotate: [number, number, number], + scale: [number, number, number] = [1, 1, 1], + ): number[] { + const flat = this.viewport.projectFlat(lnglat); + // @ts-ignore + const modelMatrix = mat4.create(); + + mat4.translate( + modelMatrix, + modelMatrix, + vec3.fromValues(flat[0], flat[1], altitude), + ); + mat4.scale( + modelMatrix, + modelMatrix, + vec3.fromValues(scale[0], scale[1], scale[2]), + ); + + mat4.rotateX(modelMatrix, modelMatrix, rotate[0]); + mat4.rotateY(modelMatrix, modelMatrix, rotate[1]); + mat4.rotateZ(modelMatrix, modelMatrix, rotate[2]); + + return modelMatrix as unknown as number[]; + } + + public getCustomCoordCenter?(): [number, number] { + throw new Error('Method not implemented.'); + } + + public exportMap(type: 'jpg' | 'png'): string { + const renderCanvas = this.getMapCanvasContainer() as HTMLCanvasElement; + const layersPng = + type === 'jpg' + ? (renderCanvas?.toDataURL('image/jpeg') as string) + : (renderCanvas?.toDataURL('image/png') as string); + return layersPng; + } + + // Method on earth mode + public rotateY?(option: { force?: boolean; reg?: number }): void { + throw new Error('Method not implemented.'); + } + + private hideLogo() { + const container = this.map.getContainer(); + if (!container) { + return; + } + DOM.addClass(container, 'tmap-contianer--hide-logo'); + } +} diff --git a/packages/maps/src/tmap/maploader.ts b/packages/maps/src/tmap/maploader.ts new file mode 100644 index 0000000000..06dfd473bd --- /dev/null +++ b/packages/maps/src/tmap/maploader.ts @@ -0,0 +1,113 @@ +/* eslint-disable */ +if (!window) { + throw Error('TMap JSAPI can only be used in Browser.'); +} + +enum LoadStatus { + notload = 'notload', + loading = 'loading', + loaded = 'loaded', + failed = 'failed', +} + +interface ILoadOption { + key: string; + version?: string; + libraries?: string[]; +} + +const config: ILoadOption = { + key: '', + version: '1.exp', + libraries: [], +}; + +let Status = { + TMap: LoadStatus.notload, +}; + +const onloadCBKs: any[] = []; +// @ts-ignore +const onload = (callback: (map: window.AMap) => void) => { + if (typeof callback === 'function') { + if (Status.TMap === LoadStatus.loaded) { + callback(window.AMap); + return; + } + onloadCBKs.push(callback); + } +}; + +const load = (options: ILoadOption) => { + return new Promise((resolve, reject) => { + if (Status.TMap === LoadStatus.failed) { + reject(''); + } else if (Status.TMap === LoadStatus.notload) { + const { key, version, libraries } = options; + if (!key) { + reject('请填写key'); + return; + } + + config.key = key; + config.version = version || config.version; + config.libraries = libraries || config.libraries; + Status.TMap = LoadStatus.loading; + + (window as any)._onTMapAPILoaded = (err: any) => { + delete (window as any)._onTMapAPILoaded; + if (err) { + Status.TMap = LoadStatus.failed; + reject(err); + } else { + Status.TMap = LoadStatus.loaded; + while (onloadCBKs.length) { + onloadCBKs.splice(0, 1)[0](window.TMap); + } + } + }; + + const script = document.createElement('script'); + script.type = 'text/javascript'; + script.async = false; + script.src = + 'https://map.qq.com/api/gljs?callback=_onTMapAPILoaded&v=' + + config.version + + '&key=' + + key + + '&plugin=' + + config.libraries!.join(','); + script.onerror = (e) => { + Status.TMap = LoadStatus.failed; + reject(e); + }; + + const parentNode = document.body || document.head; + parentNode.appendChild(script); + onload(resolve); + } else if (Status.TMap === LoadStatus.loaded) { + if (options.key && options.key !== config.key) { + reject('多个不一致的 key'); + return; + } + + if (options.version && options.version !== config.version) { + reject('不允许多个版本 JSAPI 混用'); + return; + } + + onload(resolve); + } + }); +}; + +const reset = () => { + // @ts-ignore + delete window.TMap; + + Status = { + TMap: LoadStatus.notload, + }; +}; + +export default { load, reset }; diff --git a/packages/maps/typings/index.d.ts b/packages/maps/typings/index.d.ts index 6b8df3b69a..e10cd16d71 100644 --- a/packages/maps/typings/index.d.ts +++ b/packages/maps/typings/index.d.ts @@ -1,5 +1,6 @@ /// /// +/// import { IControl } from 'mapbox-gl'; interface Window { diff --git a/packages/site/examples/tutorial/map/demo/meta.json b/packages/site/examples/tutorial/map/demo/meta.json index 272beaef19..246ed9f1cb 100644 --- a/packages/site/examples/tutorial/map/demo/meta.json +++ b/packages/site/examples/tutorial/map/demo/meta.json @@ -23,6 +23,17 @@ { "filename": "bmapInstance.js", "title": "百度地图实例化", + "screenshot": "https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*A4BOR4hBNcUAAAAAAAAAAAAAARQnAQ" + }, + { + "filename": "tencentmap.js", + "title": "腾讯底图", + "new": true, + "screenshot": "https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*C4BvT5gcclMAAAAAAAAAAAAAARQnAQ" + }, + { + "filename": "tmapInstance.js", + "title": "腾讯地图实例化", "new": true, "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*2v8KSpMdVK0AAAAAAAAAAAAADmJ7AQ/original" } diff --git a/packages/site/examples/tutorial/map/demo/tencentmap.js b/packages/site/examples/tutorial/map/demo/tencentmap.js new file mode 100644 index 0000000000..c90a004eef --- /dev/null +++ b/packages/site/examples/tutorial/map/demo/tencentmap.js @@ -0,0 +1,11 @@ +import { Scene } from '@antv/l7'; +import { TencentMap } from '@antv/l7-maps'; + +new Scene({ + id: 'map', + map: new TencentMap({ + style: 'style1', + center: [ 107.054293, 35.246265 ], + zoom: 4.056 + }) +}); diff --git a/packages/site/examples/tutorial/map/demo/tmapInstance.js b/packages/site/examples/tutorial/map/demo/tmapInstance.js new file mode 100644 index 0000000000..5c2b9fa79b --- /dev/null +++ b/packages/site/examples/tutorial/map/demo/tmapInstance.js @@ -0,0 +1,48 @@ +import { Scene, PointLayer } from '@antv/l7'; +import { TencentMap } from '@antv/l7-maps'; + +function initMap() { + const scene = new Scene({ + id: 'map', + map: new TencentMap({ + zoom: 10, + minZoom: 5, + maxZoom: 18 + }) + }); + scene.on('loaded', () => { + fetch( + 'https://gw.alipayobjects.com/os/basement_prod/893d1d5f-11d9-45f3-8322-ee9140d288ae.json' + ) + .then(res => res.json()) + .then(data => { + const pointLayer = new PointLayer() + .source(data, { + parser: { + type: 'json', + x: 'longitude', + y: 'latitude' + } + }) + .shape('name', [ + 'circle', + 'triangle', + 'square', + 'pentagon', + 'hexagon', + 'octogon', + 'hexagram', + 'rhombus', + 'vesica' + ]) + .size('unit_price', [10, 25]) + .color('name', ['#5B8FF9', '#5CCEA1', '#5D7092', '#F6BD16', '#E86452']) + .style({ + opacity: 0.3, + strokeWidth: 2 + }); + scene.addLayer(pointLayer); + }); + }); +} +initMap();