diff --git a/.eslintrc.js b/.eslintrc.js index f3a3cd327b..7c11b791ce 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -41,4 +41,4 @@ module.exports = { }, }, } -}; \ No newline at end of file +}; diff --git a/dev-demos/layer/control/exportImage.md b/dev-demos/layer/control/exportImage.md new file mode 100644 index 0000000000..601b0ae789 --- /dev/null +++ b/dev-demos/layer/control/exportImage.md @@ -0,0 +1,6 @@ +--- +title: 导出图片 +order: 9 +--- + + diff --git a/dev-demos/layer/control/exportImage.tsx b/dev-demos/layer/control/exportImage.tsx new file mode 100644 index 0000000000..f46f661d4c --- /dev/null +++ b/dev-demos/layer/control/exportImage.tsx @@ -0,0 +1,86 @@ +import { GaodeMapV2, Scene, ExportImage, PointLayer } from '@antv/l7'; +import React, { useState } from 'react'; +// tslint:disable-next-line:no-duplicate-imports +import { FunctionComponent, useEffect } from 'react'; + +const Demo: FunctionComponent = () => { + const [scene, setScene] = useState(); + const [imgSrc, setImgSrc] = useState(''); + const [control, setControl] = useState(null); + + useEffect(() => { + const newScene = new Scene({ + id: 'map', + map: new GaodeMapV2({ + style: 'normal', + center: [120, 30], + pitch: 0, + zoom: 6.45, + WebGLParams: { + preserveDrawingBuffer: true, + }, + }), + // logoVisible: false, + }); + + newScene.on('loaded', () => { + const newControl = new ExportImage({ + onExport: (base64) => { + setImgSrc(base64); + }, + }); + newScene.addControl(newControl); + setControl(newControl); + + fetch( + 'https://gw.alipayobjects.com/os/basement_prod/d3564b06-670f-46ea-8edb-842f7010a7c6.json', + ) + .then((res) => res.json()) + .then((data) => { + const pointLayer = new PointLayer({ + autoFit: true, + }) + .source(data) + .shape('circle') + .size('mag', [1, 25]) + .color('mag', (mag) => { + return mag > 4.5 ? '#5B8FF9' : '#5CCEA1'; + }) + .active(true) + .style({ + opacity: 0.3, + strokeWidth: 1, + }); + newScene.addLayer(pointLayer); + setScene(newScene); + }); + }); + }, []); + + return ( + <> + +
+
+
截图展示:
+ +
+ + ); +}; + +export default Demo; diff --git a/dev-demos/layer/control/fullscreen.md b/dev-demos/layer/control/fullscreen.md new file mode 100644 index 0000000000..563a42780f --- /dev/null +++ b/dev-demos/layer/control/fullscreen.md @@ -0,0 +1,6 @@ +--- +title: 全屏 +order: 8 +--- + + diff --git a/dev-demos/layer/control/fullscreen.tsx b/dev-demos/layer/control/fullscreen.tsx new file mode 100644 index 0000000000..7fb7015703 --- /dev/null +++ b/dev-demos/layer/control/fullscreen.tsx @@ -0,0 +1,38 @@ +import { GaodeMap, Scene, Fullscreen } from '@antv/l7'; +import React from 'react'; +// tslint:disable-next-line:no-duplicate-imports +import { FunctionComponent, useEffect } from 'react'; + +const Demo: FunctionComponent = () => { + useEffect(() => { + const scene = new Scene({ + id: 'map', + map: new GaodeMap({ + style: 'normal', + center: [120, 30], + pitch: 0, + zoom: 6.45, + }), + // logoVisible: false, + }); + + scene.on('loaded', () => { + const newFullscreen = new Fullscreen(); + scene.addControl(newFullscreen); + }); + }, []); + + return ( + <> +
+ + ); +}; + +export default Demo; diff --git a/dev-demos/layer/control/hide.md b/dev-demos/layer/control/hide.md new file mode 100644 index 0000000000..378ab1648a --- /dev/null +++ b/dev-demos/layer/control/hide.md @@ -0,0 +1,6 @@ +--- +title: 显示/隐藏 +order: 3 +--- + + diff --git a/dev-demos/layer/control/hide.tsx b/dev-demos/layer/control/hide.tsx new file mode 100644 index 0000000000..236420a194 --- /dev/null +++ b/dev-demos/layer/control/hide.tsx @@ -0,0 +1,59 @@ +import { GaodeMap, PositionType, Scene, MapTheme } from '@antv/l7'; +import React, { useState } from 'react'; +// tslint:disable-next-line:no-duplicate-imports +import { FunctionComponent, useEffect } from 'react'; + +const POSITION_LIST = Object.values(PositionType); + +const Demo: FunctionComponent = () => { + const [zoom, setZoom] = useState(() => { + return new MapTheme({ + position: 'topleft', + }); + }); + + useEffect(() => { + const scene = new Scene({ + id: 'map', + map: new GaodeMap({ + style: 'dark', + center: [120, 30], + pitch: 0, + zoom: 6.45, + }), + // logoVisible: false, + }); + + scene.on('loaded', () => { + scene.addControl(zoom); + }); + }, []); + + return ( + <> + + +
+ + ); +}; + +export default Demo; diff --git a/dev-demos/layer/control/layerControl.md b/dev-demos/layer/control/layerControl.md new file mode 100644 index 0000000000..482174e490 --- /dev/null +++ b/dev-demos/layer/control/layerControl.md @@ -0,0 +1,6 @@ +--- +title: 图层控制 +order: 13 +--- + + diff --git a/dev-demos/layer/control/layerControl.tsx b/dev-demos/layer/control/layerControl.tsx new file mode 100644 index 0000000000..a8b41daef6 --- /dev/null +++ b/dev-demos/layer/control/layerControl.tsx @@ -0,0 +1,148 @@ +import { + GaodeMapV2, + Scene, + LayerControl, + ILayer, + PointLayer, + LineLayer, + PolygonLayer, +} from '@antv/l7'; +import React, { useState } from 'react'; +// tslint:disable-next-line:no-duplicate-imports +import { FunctionComponent, useEffect } from 'react'; + +const Demo: FunctionComponent = () => { + const [layers, setLayers] = useState([]); + const [scene, setScene] = useState(); + const [newLayer, setNewLayer] = useState(null); + const [control, setControl] = useState(null); + + useEffect(() => { + const newScene = new Scene({ + id: 'map', + map: new GaodeMapV2({ + style: 'normal', + center: [120, 30], + pitch: 0, + zoom: 6.45, + }), + // logoVisible: false, + }); + + newScene.on('loaded', () => { + const newLayers: ILayer[] = []; + window.Promise.all([ + fetch( + 'https://gw.alipayobjects.com/os/basement_prod/d3564b06-670f-46ea-8edb-842f7010a7c6.json', + ) + .then((res) => res.json()) + .then((data) => { + const pointLayer = new PointLayer({ + name: '点图层', + autoFit: true, + }) + .source(data) + .shape('circle') + .size('mag', [1, 25]) + .color('mag', (mag) => { + return mag > 4.5 ? '#5B8FF9' : '#5CCEA1'; + }) + .active(true) + .style({ + opacity: 0.3, + strokeWidth: 1, + }); + setNewLayer(pointLayer); + newScene.addLayer(pointLayer); + }), + fetch( + // 'https://gw.alipayobjects.com/os/bmw-prod/1981b358-28d8-4a2f-9c74-a857d5925ef1.json' // 获取行政区划P噢利用 + 'https://gw.alipayobjects.com/os/bmw-prod/d6da7ac1-8b4f-4a55-93ea-e81aa08f0cf3.json', + ) + .then((res) => res.json()) + .then((data) => { + const chinaPolygonLayer = new PolygonLayer({ + name: '中国填充', + autoFit: true, + }) + .source(data) + .color('name', [ + 'rgb(239,243,255)', + 'rgb(189,215,231)', + 'rgb(107,174,214)', + 'rgb(49,130,189)', + 'rgb(8,81,156)', + ]) + .shape('fill') + .style({ + opacity: 1, + }); + // 图层边界 + const layer2 = new LineLayer({ + name: '中国边框', + zIndex: 2, + }) + .source(data) + .color('rgb(93,112,146)') + .size(0.6) + .style({ + opacity: 1, + }); + + layer2.hide(); + + newScene.addLayer(chinaPolygonLayer); + newScene.addLayer(layer2); + newLayers.push(chinaPolygonLayer, layer2); + }), + ]).then(() => { + const newControl = new LayerControl({ + layers: newLayers, + }); + setControl(newControl); + newScene.addControl(newControl); + setLayers(newLayers); + setScene(newScene); + }); + }); + }, []); + + return ( + <> + + +
+ + ); +}; + +export default Demo; diff --git a/dev-demos/layer/control/logo.md b/dev-demos/layer/control/logo.md new file mode 100644 index 0000000000..9fe0a0f28c --- /dev/null +++ b/dev-demos/layer/control/logo.md @@ -0,0 +1,6 @@ +--- +title: Logo +order: 10 +--- + + diff --git a/dev-demos/layer/control/logo.tsx b/dev-demos/layer/control/logo.tsx new file mode 100644 index 0000000000..1e38aa1bae --- /dev/null +++ b/dev-demos/layer/control/logo.tsx @@ -0,0 +1,73 @@ +import { GaodeMap, Scene, Logo } from '@antv/l7'; +import React from 'react'; +// tslint:disable-next-line:no-duplicate-imports +import { FunctionComponent, useEffect } from 'react'; + +const Demo: FunctionComponent = () => { + useEffect(() => { + const scene = new Scene({ + id: 'map', + map: new GaodeMap({ + style: 'dark', + center: [120, 30], + pitch: 0, + zoom: 6.45, + }), + logoVisible: false, + }); + + scene.on('loaded', () => { + const logo1 = new Logo({ + position: 'leftbottom', + }); + scene.addControl(logo1); + + setTimeout(() => { + logo1.setOptions({ + img: + 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg', + href: '', + style: 'height: 40px; width: 40px;', + }); + }, 1000); + + const logo2 = new Logo({ + position: 'rightbottom', + href: undefined, + }); + scene.addControl(logo2); + + const logo3 = new Logo({ + position: 'topright', + style: 'height: 40px; width: 40px;', + img: + 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg', + href: '', + }); + scene.addControl(logo3); + + const logo4 = new Logo({ + position: 'topleft', + style: 'height: 40px; width: 40px;', + img: + 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg', + href: 'https://ant.design/index-cn', + }); + scene.addControl(logo4); + }); + }, []); + + return ( + <> +
+ + ); +}; + +export default Demo; diff --git a/dev-demos/layer/control/mapTheme.md b/dev-demos/layer/control/mapTheme.md new file mode 100644 index 0000000000..5a673f7d32 --- /dev/null +++ b/dev-demos/layer/control/mapTheme.md @@ -0,0 +1,6 @@ +--- +title: 地图样式 +order: 12 +--- + + diff --git a/dev-demos/layer/control/mapTheme.tsx b/dev-demos/layer/control/mapTheme.tsx new file mode 100644 index 0000000000..7e44e1cc77 --- /dev/null +++ b/dev-demos/layer/control/mapTheme.tsx @@ -0,0 +1,53 @@ +import { GaodeMap, Scene, MapTheme } from '@antv/l7'; +import React, { useState } from 'react'; +// tslint:disable-next-line:no-duplicate-imports +import { FunctionComponent, useEffect } from 'react'; + +const Demo: FunctionComponent = () => { + const [scene, setScene] = useState(); + + useEffect(() => { + const newScene = new Scene({ + id: 'map', + map: new GaodeMap({ + center: [120, 30], + pitch: 0, + zoom: 6.45, + style: 'normal', + }), + // map: new GaodeMapV2({ + // style: 'dark', + // center: [120, 30], + // pitch: 0, + // zoom: 6.45, + // }), + // map: new GaodeMap({ + // style: 'dark', + // center: [120, 30], + // pitch: 0, + // zoom: 6.45, + // }), + }); + + newScene.on('loaded', () => { + const newControl = new MapTheme({ + // defaultValue: 'normal', + }); + newScene.addControl(newControl); + }); + }, []); + + return ( + <> +
+ + ); +}; + +export default Demo; diff --git a/dev-demos/layer/control/mouseLocation.md b/dev-demos/layer/control/mouseLocation.md new file mode 100644 index 0000000000..fb8b7e5b3c --- /dev/null +++ b/dev-demos/layer/control/mouseLocation.md @@ -0,0 +1,6 @@ +--- +title: 鼠标经纬度 +order: 12 +--- + + diff --git a/dev-demos/layer/control/mouseLocation.tsx b/dev-demos/layer/control/mouseLocation.tsx new file mode 100644 index 0000000000..495f62dfbd --- /dev/null +++ b/dev-demos/layer/control/mouseLocation.tsx @@ -0,0 +1,42 @@ +import { GaodeMapV2, Scene, MouseLocation } from '@antv/l7'; +import React from 'react'; +// tslint:disable-next-line:no-duplicate-imports +import { FunctionComponent, useEffect } from 'react'; + +const Demo: FunctionComponent = () => { + useEffect(() => { + const newScene = new Scene({ + id: 'map', + map: new GaodeMapV2({ + style: 'normal', + center: [120, 30], + pitch: 0, + zoom: 6.45, + }), + // logoVisible: false, + }); + + newScene.on('loaded', () => { + const newControl = new MouseLocation({}); + newScene.addControl(newControl); + // const zoom = new Zoom({ + // position: 'topright', + // }); + // newScene.addControl(zoom); + }); + }, []); + + return ( + <> +
+ + ); +}; + +export default Demo; diff --git a/dev-demos/layer/control/navigation.md b/dev-demos/layer/control/navigation.md new file mode 100644 index 0000000000..3e88b434aa --- /dev/null +++ b/dev-demos/layer/control/navigation.md @@ -0,0 +1,6 @@ +--- +title: 定位 +order: 10 +--- + + diff --git a/dev-demos/layer/control/navigation.tsx b/dev-demos/layer/control/navigation.tsx new file mode 100644 index 0000000000..6cecbe23e1 --- /dev/null +++ b/dev-demos/layer/control/navigation.tsx @@ -0,0 +1,49 @@ +import { GaodeMapV2, GeoLocate, Scene } from '@antv/l7'; +import gcoord from 'gcoord'; +import React, { useState } from 'react'; +// tslint:disable-next-line:no-duplicate-imports +import { FunctionComponent, useEffect } from 'react'; + +const Demo: FunctionComponent = () => { + const [scene, setScene] = useState(); + + useEffect(() => { + const newScene = new Scene({ + id: 'map', + map: new GaodeMapV2({ + style: 'normal', + center: [120, 30], + pitch: 0, + zoom: 6.45, + preserveDrawingBuffer: true, + // WebGLParams: { + // preserveDrawingBuffer: true, + // }, + }), + // logoVisible: false, + }); + + newScene.on('loaded', () => { + const newControl = new GeoLocate({ + transform: (position) => { + return gcoord.transform(position, gcoord.WGS84, gcoord.GCJ02); + }, + }); + newScene.addControl(newControl); + }); + }, []); + + return ( + <> +
+ + ); +}; + +export default Demo; diff --git a/dev-demos/layer/control/position.md b/dev-demos/layer/control/position.md new file mode 100644 index 0000000000..2a367348ed --- /dev/null +++ b/dev-demos/layer/control/position.md @@ -0,0 +1,6 @@ +--- +title: 控件位置 +order: 1 +--- + + diff --git a/dev-demos/layer/control/position.tsx b/dev-demos/layer/control/position.tsx new file mode 100644 index 0000000000..cfc30af3c6 --- /dev/null +++ b/dev-demos/layer/control/position.tsx @@ -0,0 +1,64 @@ +import { GaodeMap, Logo, PositionName, Scale, Scene, Zoom } from '@antv/l7'; +import { FunctionComponent, useEffect } from 'react'; + +const Demo: FunctionComponent = () => { + useEffect(() => { + const scene = new Scene({ + id: 'map', + map: new GaodeMap({ + style: 'dark', + center: [120, 30], + pitch: 0, + zoom: 6.45, + }), + // logoVisible: false, + }); + + scene.on('loaded', () => { + function createTestControl(position: PositionName) { + scene.addControl( + new Zoom({ + position, + }), + ); + scene.addControl( + new Scale({ + position, + }), + ); + + scene.addControl( + new Logo({ + position, + }), + ); + } + + createTestControl('topleft'); + createTestControl('topright'); + createTestControl('bottomleft'); + createTestControl('bottomright'); + + createTestControl('lefttop'); + createTestControl('leftbottom'); + createTestControl('righttop'); + createTestControl('rightbottom'); + + createTestControl('topcenter'); + createTestControl('leftcenter'); + createTestControl('rightcenter'); + createTestControl('bottomcenter'); + }); + }, []); + return ( +
+ ); +}; + +export default Demo; diff --git a/dev-demos/layer/control/remove.md b/dev-demos/layer/control/remove.md new file mode 100644 index 0000000000..a3b3ddfeee --- /dev/null +++ b/dev-demos/layer/control/remove.md @@ -0,0 +1,6 @@ +--- +title: 插入/移除 +order: 4 +--- + + diff --git a/dev-demos/layer/control/remove.tsx b/dev-demos/layer/control/remove.tsx new file mode 100644 index 0000000000..b8dec3ac5f --- /dev/null +++ b/dev-demos/layer/control/remove.tsx @@ -0,0 +1,65 @@ +import { GaodeMap, PositionType, Scene, Zoom } from '@antv/l7'; +import React, { useState } from 'react'; +// tslint:disable-next-line:no-duplicate-imports +import { FunctionComponent, useEffect } from 'react'; + +const POSITION_LIST = Object.values(PositionType); + +const Demo: FunctionComponent = () => { + const [scene, setScene] = useState(null); + const [zoom, setZoom] = useState(null); + + useEffect(() => { + const newScene = new Scene({ + id: 'map', + map: new GaodeMap({ + style: 'dark', + center: [120, 30], + pitch: 0, + zoom: 6.45, + }), + // logoVisible: false, + }); + + newScene.on('loaded', () => { + setScene(newScene); + }); + }, []); + + return ( + <> + + +
+ + ); +}; + +export default Demo; diff --git a/dev-demos/layer/control/scale.md b/dev-demos/layer/control/scale.md new file mode 100644 index 0000000000..b2a456ddcb --- /dev/null +++ b/dev-demos/layer/control/scale.md @@ -0,0 +1,6 @@ +--- +title: 比例尺 +order: 14 +--- + + diff --git a/dev-demos/layer/control/scale.tsx b/dev-demos/layer/control/scale.tsx new file mode 100644 index 0000000000..b7842421d8 --- /dev/null +++ b/dev-demos/layer/control/scale.tsx @@ -0,0 +1,63 @@ +import { GaodeMapV2, Scene, Scale } from '@antv/l7'; +import React, { useState } from 'react'; +// tslint:disable-next-line:no-duplicate-imports +import { FunctionComponent, useEffect } from 'react'; + +const Demo: FunctionComponent = () => { + const [control, setControl] = useState(null); + + useEffect(() => { + const newScene = new Scene({ + id: 'map', + // map: new GaodeMap({ + // center: [120, 30], + // pitch: 0, + // zoom: 6.45, + // }), + map: new GaodeMapV2({ + center: [120, 30], + pitch: 0, + zoom: 6.45, + style: 'normal', + }), + }); + + newScene.on('loaded', () => { + const scale = new Scale({ + metric: true, + position: 'rightbottom', + // imperial: true, + }); + // const zoom = new Zoom({ + // position: 'rightbottom', + // }); + newScene.addControl(scale); + // newScene.addControl(zoom); + setControl(scale); + }); + }, []); + + return ( + <> + +
+ + ); +}; + +export default Demo; diff --git a/dev-demos/layer/control/setOptions.md b/dev-demos/layer/control/setOptions.md new file mode 100644 index 0000000000..431154b3fe --- /dev/null +++ b/dev-demos/layer/control/setOptions.md @@ -0,0 +1,6 @@ +--- +title: 更新配置 +order: 2 +--- + + diff --git a/dev-demos/layer/control/setOptions.tsx b/dev-demos/layer/control/setOptions.tsx new file mode 100644 index 0000000000..4c6a21c821 --- /dev/null +++ b/dev-demos/layer/control/setOptions.tsx @@ -0,0 +1,54 @@ +import { GaodeMap, PositionType, Scene, Zoom } from '@antv/l7'; +import React, { useState } from 'react'; +// tslint:disable-next-line:no-duplicate-imports +import { FunctionComponent, useEffect } from 'react'; + +const POSITION_LIST = Object.values(PositionType); + +const Demo: FunctionComponent = () => { + const [zoom, setZoom] = useState(() => { + return new Zoom({ + position: 'topleft', + }); + }); + + useEffect(() => { + const scene = new Scene({ + id: 'map', + map: new GaodeMap({ + style: 'dark', + center: [120, 30], + pitch: 0, + zoom: 6.45, + }), + // logoVisible: false, + }); + + scene.on('loaded', () => { + scene.addControl(zoom); + }); + }, []); + + const onChangePosition = () => { + const randomIndex = Math.floor(Math.random() * POSITION_LIST.length); + zoom.setOptions({ + position: POSITION_LIST[randomIndex], + className: `random-class-${Math.floor(Math.random() * 100)}`, + }); + }; + + return ( + <> + +
+ + ); +}; + +export default Demo; diff --git a/dev-demos/layer/control/zoom.md b/dev-demos/layer/control/zoom.md new file mode 100644 index 0000000000..d5783c483b --- /dev/null +++ b/dev-demos/layer/control/zoom.md @@ -0,0 +1,6 @@ +--- +title: 缩放 +order: 13 +--- + + diff --git a/dev-demos/layer/control/zoom.tsx b/dev-demos/layer/control/zoom.tsx new file mode 100644 index 0000000000..1bf939bfc6 --- /dev/null +++ b/dev-demos/layer/control/zoom.tsx @@ -0,0 +1,77 @@ +import { GaodeMap, Scene, Zoom } from '@antv/l7'; +import React, { useState } from 'react'; +// tslint:disable-next-line:no-duplicate-imports +import { FunctionComponent, useEffect } from 'react'; + +const Demo: FunctionComponent = () => { + const [control, setControl] = useState(null); + + useEffect(() => { + const newScene = new Scene({ + id: 'map', + map: new GaodeMap({ + center: [120, 30], + pitch: 0, + zoom: 6.45, + }), + }); + + newScene.on('loaded', () => { + const newControl = new Zoom(); + newScene.addControl(newControl); + setControl(newControl); + }); + }, []); + + return ( + <> + + + + +
+ + ); +}; + +export default Demo; diff --git a/dev-demos/layer/popup/layerPopup.md b/dev-demos/layer/popup/layerPopup.md new file mode 100644 index 0000000000..45c8b75957 --- /dev/null +++ b/dev-demos/layer/popup/layerPopup.md @@ -0,0 +1,6 @@ +--- +title: 图层气泡 +order: 2 +--- + + diff --git a/dev-demos/layer/popup/layerPopup.tsx b/dev-demos/layer/popup/layerPopup.tsx new file mode 100644 index 0000000000..8aab616776 --- /dev/null +++ b/dev-demos/layer/popup/layerPopup.tsx @@ -0,0 +1,118 @@ +import { + GaodeMap, + LayerPopup, + PointLayer, + Scene, + LineLayer, + // anchorType, +} from '@antv/l7'; +import { featureCollection, point } from '@turf/turf'; +import React, { useState } from 'react'; +// tslint:disable-next-line:no-duplicate-imports +import { FunctionComponent, useEffect } from 'react'; + +const Demo: FunctionComponent = () => { + const [scene, setScene] = useState(null); + const [popup, setPopup] = useState(null); + + useEffect(() => { + const newScene = new Scene({ + id: 'map', + map: new GaodeMap({ + style: 'dark', + center: [120.104697, 30.260704], + pitch: 0, + zoom: 15, + }), + // logoVisible: false, + }); + + newScene.on('loaded', () => { + const pointLayer = new PointLayer({ + name: 'pointLayer', + }); + pointLayer + .source( + featureCollection([ + point([120.104697, 30.260704], { + name: '测试点1', + lng: 120.104697, + lat: 30.260704, + }), + point([120.104697, 30.261715], { + name: '测试点2', + lng: 120.104697, + lat: 30.261715, + }), + ]), + ) + .color('#ff0000') + .size(10); + const lineString = new LineLayer({ + name: 'lineLayer', + }); + lineString + .source( + featureCollection([ + { + type: 'Feature', + properties: { + name: '测试线3', + }, + geometry: { + type: 'LineString', + coordinates: [ + [120.103615, 30.262026], + [120.103172, 30.261771], + [120.102697, 30.261934], + ], + }, + }, + ]), + ) + .size(6) + .color('#00ff00'); + newScene.addLayer(pointLayer); + newScene.addLayer(lineString); + const newPopup = new LayerPopup({ + config: [ + { + layer: 'pointLayer', + fields: [ + { + field: 'name', + formatField: (key) => { + return '名称'; + }, + }, + 'lng', + 'lat', + ], + }, + { + layer: 'lineLayer', + fields: ['name'], + }, + ], + trigger: 'hover', + }); + newScene.addPopup(newPopup); + setPopup(newPopup); + setScene(newScene); + }); + }, []); + + return ( + <> +
+ + ); +}; + +export default Demo; diff --git a/dev-demos/layer/popup/popup.md b/dev-demos/layer/popup/popup.md new file mode 100644 index 0000000000..acfe5fef7d --- /dev/null +++ b/dev-demos/layer/popup/popup.md @@ -0,0 +1,6 @@ +--- +title: 气泡 +order: 1 +--- + + diff --git a/dev-demos/layer/popup/popup.tsx b/dev-demos/layer/popup/popup.tsx new file mode 100644 index 0000000000..3f0005b230 --- /dev/null +++ b/dev-demos/layer/popup/popup.tsx @@ -0,0 +1,256 @@ +import { + GaodeMap, + PointLayer, + Popup, + Scene, + Fullscreen, + anchorType, + // anchorType, +} from '@antv/l7'; +import { featureCollection, point } from '@turf/turf'; +import React, { useState } from 'react'; +// tslint:disable-next-line:no-duplicate-imports +import { FunctionComponent, useEffect } from 'react'; + +const Demo: FunctionComponent = () => { + const [scene, setScene] = useState(null); + const [popup, setPopup] = useState(null); + + useEffect(() => { + const newScene = new Scene({ + id: 'map', + map: new GaodeMap({ + style: 'dark', + center: [120.104697, 30.260704], + pitch: 0, + zoom: 15, + }), + // logoVisible: false, + }); + + newScene.on('loaded', () => { + const newPopup = new Popup({ + closeOnClick: true, + closeOnEsc: true, + lngLat: { + lng: 120.104697, + lat: 30.260704, + }, + anchor: 'bottom-right', + title: 'Popup Title', + html: 'Popup Content', + }); + newScene.addPopup(newPopup); + + const pointLayer = new PointLayer(); + pointLayer + .source(featureCollection([point([120.104697, 30.260704])])) + .color('#ff0000') + .size(10); + + newScene.addLayer(pointLayer); + setPopup(newPopup); + + const fullscreen = new Fullscreen(); + newScene.addControl(fullscreen); + + setScene(newScene); + }); + }, []); + + return ( + <> +
+ + + + + + + + + + + + + + + + + + + +
+
+ + ); +}; + +export default Demo; diff --git a/dev-demos/layer/scene/boxSelect.md b/dev-demos/layer/scene/boxSelect.md new file mode 100644 index 0000000000..40f65759d5 --- /dev/null +++ b/dev-demos/layer/scene/boxSelect.md @@ -0,0 +1,6 @@ +--- +title: Scene +order: 3 +--- + + diff --git a/dev-demos/layer/scene/boxSelect.tsx b/dev-demos/layer/scene/boxSelect.tsx new file mode 100644 index 0000000000..b5b16749aa --- /dev/null +++ b/dev-demos/layer/scene/boxSelect.tsx @@ -0,0 +1,70 @@ +import { GaodeMap, Scene } from '@antv/l7'; +import React, { useState } from 'react'; +// tslint:disable-next-line:no-duplicate-imports +import { FunctionComponent, useEffect } from 'react'; + +const Demo: FunctionComponent = () => { + const [scene, setScene] = useState(null); + + useEffect(() => { + const newScene = new Scene({ + id: 'map', + map: new GaodeMap({ + style: 'dark', + center: [120.104697, 30.260704], + pitch: 0, + zoom: 15, + }), + // logoVisible: false, + }); + + newScene.on('loaded', () => { + setScene(newScene); + + newScene.on('selectstart', (...params) => { + // tslint:disable-next-line:no-console + console.log('selectstart', ...params); + }); + + newScene.on('selecting', (...params) => { + // tslint:disable-next-line:no-console + console.log('selecting', ...params); + }); + + newScene.on('selectend', (...params) => { + // tslint:disable-next-line:no-console + console.log('selectend', ...params); + }); + }); + }, []); + + return ( + <> +
+ + +
+
+ + ); +}; + +export default Demo; diff --git a/jest.config.js b/jest.config.js index f551c1684b..dd43f67abb 100644 --- a/jest.config.js +++ b/jest.config.js @@ -29,23 +29,31 @@ module.exports = { babelConfig: require('./babel.config.js'), }, }, - moduleFileExtensions: [ 'ts', 'tsx', 'js' ], - modulePathIgnorePatterns: [ 'dist' ], + moduleFileExtensions: ['ts', 'tsx', 'js'], + modulePathIgnorePatterns: ['dist'], moduleNameMapper: { - '@antv/l7-(.+)$': 'packages/$1/src' + '@antv/l7-(.+)$': 'packages/$1/src', }, notify: true, notifyMode: 'always', - roots: [ 'packages' ], - testMatch: [ '**/__tests__/*.spec.+(ts|tsx|js)', '**/*.test.+(ts|tsx|js)', '**/__tests__/*/*.spec.+(ts|tsx|js)' ], + roots: ['packages'], + testMatch: [ + '**/__tests__/*.spec.+(ts|tsx|js)', + '**/*.test.+(ts|tsx|js)', + '**/__tests__/*/*.spec.+(ts|tsx|js)', + ], transform: { // '^.+\\.(ts|tsx)$': 'ts-jest', // @see https://github.com/kulshekhar/ts-jest/issues/1130 - '^.+\\.(ts|tsx)$': 'babel-jest' + '^.+\\.(ts|tsx)$': 'babel-jest', + screenfull: 'babel-jest', + '\\.(less|css)$': 'jest-less-loader', + '\\.png$': 'jest-file-loader', }, setupFilesAfterEnv: [ 'jest/setupTests.ts' ], snapshotSerializers: [ 'enzyme-to-json/serializer' ], coverageReporters: ['html', 'lcov', 'clover'], + coveragePathIgnorePatterns: ['/node_modules/', '/iconfont/'], coverageThreshold: { global: { branches: 9, diff --git a/package.json b/package.json index 4cbadba450..2029b844e5 100644 --- a/package.json +++ b/package.json @@ -101,10 +101,13 @@ "husky": "^3.0.9", "jest": "^24.9.0", "jest-canvas-mock": "^2.4.0", + "jest-file-loader": "^1.0.2", + "jest-less-loader": "^0.1.2", "jest-styled-components": "^6.2.1", "leaflet": "^1.8.0", "lerc": "^3.0.0", "lerna": "^3.16.4", + "less": "^4.1.3", "lint-staged": "^9.2.4", "mockjs": "^1.1.0", "npm-run-all": "^4.1.5", @@ -153,7 +156,8 @@ "webpack-dev-server": "^3.1.7", "webpack-merge": "^4.1.4", "worker-loader": "^2.0.0", - "yorkie": "^2.0.0" + "yorkie": "^2.0.0", + "gcoord": "^0.3.2" }, "scripts": { "dev": "npm run worker && dumi dev", @@ -221,6 +225,5 @@ }, "tnpm": { "mode": "yarn" - }, - "dependencies": {} + } } diff --git a/packages/component/__tests__/buttonControl.spec.ts b/packages/component/__tests__/buttonControl.spec.ts new file mode 100644 index 0000000000..6ba56854ec --- /dev/null +++ b/packages/component/__tests__/buttonControl.spec.ts @@ -0,0 +1,54 @@ +import { TestScene } from '@antv/l7-test-utils'; +import ButtonControl from '../src/control/baseControl/buttonControl'; +import { createL7Icon } from '../src/utils/icon'; + +class TestControl extends ButtonControl {} + +describe('buttonControl', () => { + const scene = TestScene(); + + it('life cycle', () => { + const control = new TestControl(); + scene.addControl(control); + + const container = control.getContainer(); + expect(container.parentElement).toBeInstanceOf(HTMLElement); + + scene.removeControl(control); + expect(container.parentElement).not.toBeInstanceOf(HTMLElement); + }); + + it('disable', () => { + const control = new TestControl(); + scene.addControl(control); + control.setIsDisable(true); + + expect(control.getContainer().getAttribute('disabled')).not.toBeNull(); + control.setIsDisable(false); + + expect(control.getContainer().getAttribute('disabled')).toBeNull(); + }); + + it('options', () => { + const control = new TestControl({ + title: '导出图片', + btnText: '导出图片', + btnIcon: createL7Icon('l7-icon-tupian'), + }); + scene.addControl(control); + + const container = control.getContainer(); + expect(container.classList).toContain('l7-button-control'); + expect(container.getAttribute('title')).toContain('导出图片'); + const textContainer = container.querySelector('.l7-button-control__text')!; + expect(textContainer).toBeInstanceOf(HTMLElement); + + control.setOptions({ + title: undefined, + btnText: '替换文本', + btnIcon: createL7Icon('l7-icon-tupian1'), + }); + + expect(container.getAttribute('title')).toBeFalsy(); + }); +}); diff --git a/packages/component/__tests__/control.spec.ts b/packages/component/__tests__/control.spec.ts new file mode 100644 index 0000000000..32f0670dbe --- /dev/null +++ b/packages/component/__tests__/control.spec.ts @@ -0,0 +1,68 @@ +import { TestScene } from '@antv/l7-test-utils'; +import { DOM } from '@antv/l7-utils'; +import { Control } from '../src/control/baseControl'; + +class TestControl extends Control { + public onAdd(): HTMLElement { + return DOM.create('div'); + } + public onRemove(): void {} +} + +describe('control', () => { + const scene = TestScene(); + + it('life cycle', () => { + const className1 = 'testControl1'; + const className2 = 'testControl2'; + const control1 = new TestControl({ + className: className1, + }); + const control2 = new TestControl({ + className: className2, + }); + scene.addControl(control1); + scene.addControl(control2); + + const dom1 = document.querySelector(`.${className1}`); + expect(dom1).toBeInstanceOf(HTMLElement); + const dom2 = document.querySelector(`.${className2}`); + expect(dom2).toBeInstanceOf(HTMLElement); + + scene.removeControl(control1); + scene.removeControl(control2); + const dom3 = document.querySelector(`.${className1}`); + expect(dom3).toBeNull(); + const dom4 = document.querySelector(`.${className2}`); + expect(dom4).toBeNull(); + }); + + it('show hide', () => { + const control = new TestControl(); + scene.addControl(control); + control.hide(); + expect(control.getContainer().classList).toContain('l7-control--hide'); + expect(control.getIsShow()).toEqual(false); + control.show(); + expect(control.getContainer().classList).not.toContain('l7-control--hide'); + expect(control.getIsShow()).toEqual(true); + }); + + it('options', () => { + const className = 'gunala'; + const color = 'rgb(255, 0, 0)'; + const control = new TestControl({}); + scene.addControl(control); + control.setOptions({ + position: 'leftbottom', + className, + style: `color: ${color};`, + }); + const container = control.getContainer(); + const corner = container.parentElement!; + expect(corner.classList).toContain('l7-left'); + expect(corner.classList).toContain('l7-bottom'); + expect(container.classList).toContain(className); + expect(container.style.color).toEqual(color); + }); +}); diff --git a/packages/component/__tests__/exportImage.spec.ts b/packages/component/__tests__/exportImage.spec.ts new file mode 100644 index 0000000000..7ce43e623c --- /dev/null +++ b/packages/component/__tests__/exportImage.spec.ts @@ -0,0 +1,32 @@ +import { TestScene } from '@antv/l7-test-utils'; +import ExportImage from '../src/control/exportImage'; + +describe('exportImage', () => { + const scene = TestScene(); + + it('life cycle', () => { + const control = new ExportImage({}); + scene.addControl(control); + + const container = control.getContainer(); + expect(container.parentElement).toBeInstanceOf(HTMLElement); + + scene.removeControl(control); + expect(container.parentElement).not.toBeInstanceOf(HTMLElement); + }); + + it('image', () => { + const control = new ExportImage({ + onExport: (base64) => { + // tslint:disable-next-line:no-console + // console.log(base64); + }, + }); + scene.addControl(control); + + const button = control.getContainer() as HTMLDivElement; + button.click(); + + expect(button.parentElement).toBeInstanceOf(HTMLElement); + }); +}); diff --git a/packages/component/__tests__/fullscreen.spec.ts b/packages/component/__tests__/fullscreen.spec.ts new file mode 100644 index 0000000000..1488bfb6b7 --- /dev/null +++ b/packages/component/__tests__/fullscreen.spec.ts @@ -0,0 +1,27 @@ +import { TestScene } from '@antv/l7-test-utils'; +import Fullscreen from '../src/control/fullscreen'; + +describe('fullscreen', () => { + const scene = TestScene(); + + it('life cycle', () => { + const control = new Fullscreen({}); + scene.addControl(control); + + const container = control.getContainer(); + expect(container.parentElement).toBeInstanceOf(HTMLElement); + + scene.removeControl(control); + expect(container.parentElement).not.toBeInstanceOf(HTMLElement); + }); + + it('fullscreen', () => { + const control = new Fullscreen({}); + scene.addControl(control); + + const button = control.getContainer() as HTMLDivElement; + button.click(); + + expect(button.parentElement).toBeInstanceOf(HTMLElement); + }); +}); diff --git a/packages/component/__tests__/layerControl.spec.ts b/packages/component/__tests__/layerControl.spec.ts new file mode 100644 index 0000000000..6f9881aaba --- /dev/null +++ b/packages/component/__tests__/layerControl.spec.ts @@ -0,0 +1,19 @@ +import { TestScene } from '@antv/l7-test-utils'; +import LayerControl from '../src/control/layerControl'; + +describe('layerControl', () => { + const scene = TestScene(); + + it('life cycle', () => { + const layerControl = new LayerControl(); + scene.addControl(layerControl); + + const container = layerControl.getContainer(); + expect(container.parentElement).toBeInstanceOf(HTMLElement); + + expect(layerControl.getLayerVisible()).toEqual([]); + + scene.removeControl(layerControl); + expect(container.parentElement).not.toBeInstanceOf(HTMLElement); + }); +}); diff --git a/packages/component/__tests__/layerPopup.spec.ts b/packages/component/__tests__/layerPopup.spec.ts new file mode 100644 index 0000000000..6736147df7 --- /dev/null +++ b/packages/component/__tests__/layerPopup.spec.ts @@ -0,0 +1,43 @@ +import { PointLayer } from '@antv/l7-layers'; +import { TestScene } from '@antv/l7-test-utils'; +import LayerPopup from '../src/popup/layerPopup'; + +describe('popup', () => { + const scene = TestScene(); + const testClassName = 'l7-layer-popup-test'; + + it('life cycle', () => { + const pointLayer = new PointLayer(); + pointLayer.source([{ lng: 120, lat: 30 }], { + parser: { + type: 'json', + x: 'lng', + y: 'lat', + }, + }); + const layerPopup = new LayerPopup({ + className: testClassName, + config: [ + { + layer: pointLayer, + fields: [ + { + field: 'lng', + }, + ], + }, + ], + }); + scene.addPopup(layerPopup); + + expect(layerPopup.isOpen()).toEqual(true); + + layerPopup.setOptions({ + trigger: 'click', + }); + + scene.removePopup(layerPopup); + + expect(layerPopup.isOpen()).toEqual(false); + }); +}); diff --git a/packages/component/__tests__/mapTheme.spec.ts b/packages/component/__tests__/mapTheme.spec.ts new file mode 100644 index 0000000000..ab3edb55ed --- /dev/null +++ b/packages/component/__tests__/mapTheme.spec.ts @@ -0,0 +1,41 @@ +import { TestScene } from '@antv/l7-test-utils'; +import MapTheme from '../src/control/mapTheme'; + +describe('mapTheme', () => { + const scene = TestScene(); + + it('life cycle', () => { + const control = new MapTheme({}); + scene.addControl(control); + + const container = control.getContainer(); + expect(container.parentElement).toBeInstanceOf(HTMLElement); + + scene.removeControl(control); + expect(container.parentElement).not.toBeInstanceOf(HTMLElement); + }); + + it('mapTheme', () => { + const control = new MapTheme({ + defaultValue: 'normal', + }); + scene.addControl(control); + + const options = control.getOptions().options; + expect(options.length).toBeGreaterThan(0); + expect(control.getSelectValue()).toEqual( + 'mapbox://styles/mapbox/streets-v11', + ); + + const optionList = ((control + .getPopper() + .getContent() as HTMLDivElement).querySelectorAll( + '.l7-select-control-item', + ) as unknown) as HTMLDivElement[]; + optionList[1].click(); + + // expect(control.getSelectValue()).toEqual( + // 'mapbox://styles/zcxduo/ck2ypyb1r3q9o1co1766dex29', + // ); + }); +}); diff --git a/packages/component/__tests__/marker.spec.tsx b/packages/component/__tests__/marker.spec.tsx index 7b02d57c03..5ec0845747 100644 --- a/packages/component/__tests__/marker.spec.tsx +++ b/packages/component/__tests__/marker.spec.tsx @@ -1,15 +1,14 @@ +import { TestScene } from '@antv/l7-test-utils'; import Marker from '../src/marker'; -import Popup from '../src/popup'; -import { TestScene } from '@antv/l7-test-utils' +import Popup from '../src/popup/popup'; -const popup = new Popup({ offsets: [0, 20] }) - .setHTML('

111

'); +const popup = new Popup({ offsets: [0, 20] }).setHTML( + '

111

', +); -const marker = new Marker() - .setLnglat({ lng: 120, lat: 30 }) - .setPopup(popup); +const marker = new Marker().setLnglat({ lng: 120, lat: 30 }).setPopup(popup); -TestScene().addMarker(marker) +TestScene().addMarker(marker); describe('Marker', () => { it('render and remove correctly', () => { @@ -18,7 +17,7 @@ describe('Marker', () => { expect(marker.getDefault().color).toEqual('#5B8FF9'); expect(marker.getOffset()).toEqual([0, 0]); expect(marker.isDraggable()).toEqual(false); - marker.remove() + marker.remove(); expect(document.querySelector('.l7-marker')).toBeFalsy(); }); @@ -34,13 +33,13 @@ describe('Marker', () => { marker.closePopup(); expect(marker.getPopup().isOpen()).toBeFalsy(); - }) + }); it('longitude and latitude', () => { const { lng, lat } = marker.getLnglat(); expect(lng).toEqual(120); expect(lat).toEqual(30); - marker.setLnglat({ lng: 121, lat: 31 }) + marker.setLnglat({ lng: 121, lat: 31 }); const { lng: newLng, lat: newLat } = marker.getLnglat(); expect(newLng).toEqual(121); expect(newLat).toEqual(31); @@ -55,7 +54,7 @@ describe('Marker', () => { }); it('extData', () => { - marker.setExtData({ test: 1 }) - expect(marker.getExtData()).toEqual({ test: 1 }) + marker.setExtData({ test: 1 }); + expect(marker.getExtData()).toEqual({ test: 1 }); }); }); diff --git a/packages/component/__tests__/mouseLocation.spec.ts b/packages/component/__tests__/mouseLocation.spec.ts new file mode 100644 index 0000000000..a4d2413ac0 --- /dev/null +++ b/packages/component/__tests__/mouseLocation.spec.ts @@ -0,0 +1,31 @@ +import { TestScene } from '@antv/l7-test-utils'; +import MouseLocation from '../src/control/mouseLocation'; + +describe('buttonControl', () => { + const scene = TestScene(); + + it('life cycle', () => { + const control = new MouseLocation({}); + scene.addControl(control); + + const container = control.getContainer(); + expect(container.parentElement).toBeInstanceOf(HTMLElement); + + scene.removeControl(control); + expect(container.parentElement).not.toBeInstanceOf(HTMLElement); + }); + + it('life cycle', () => { + const control = new MouseLocation(); + scene.addControl(control); + + (scene.getMapService().map as any).emit('mousemove', { + lngLat: { + lng: 120, + lat: 30, + }, + }); + + expect(control.getLocation()).toEqual([120, 30]); + }); +}); diff --git a/packages/component/__tests__/navigation.spec.ts b/packages/component/__tests__/navigation.spec.ts new file mode 100644 index 0000000000..f10535ab01 --- /dev/null +++ b/packages/component/__tests__/navigation.spec.ts @@ -0,0 +1,16 @@ +import { TestScene } from '@antv/l7-test-utils'; +import Navigation from '../src/control/geoLocate'; + +describe('navigation', () => { + const scene = TestScene(); + + it('navigation', () => { + const control = new Navigation({}); + scene.addControl(control); + + const button = control.getContainer() as HTMLDivElement; + button.click(); + + expect(button.parentElement).toBeInstanceOf(HTMLElement); + }); +}); diff --git a/packages/component/__tests__/popperControl.spec.ts b/packages/component/__tests__/popperControl.spec.ts new file mode 100644 index 0000000000..6c32d4fa15 --- /dev/null +++ b/packages/component/__tests__/popperControl.spec.ts @@ -0,0 +1,38 @@ +import { TestScene } from '@antv/l7-test-utils'; +import PopperControl from '../src/control/baseControl/popperControl'; + +class TestControl extends PopperControl {} + +describe('popperControl', () => { + const scene = TestScene(); + + it('life cycle', () => { + const control = new TestControl({}); + scene.addControl(control); + + const container = control.getContainer(); + expect(container.parentElement).toBeInstanceOf(HTMLElement); + + scene.removeControl(control); + expect(container.parentElement).not.toBeInstanceOf(HTMLElement); + }); + + it('popper', () => { + const control = new TestControl({ + popperTrigger: 'click', + }); + scene.addControl(control); + }); + + it('options', () => { + const control = new TestControl({}); + scene.addControl(control); + const testClassName = 'testPopper'; + control.setOptions({ + popperClassName: testClassName, + }); + expect(control.getPopper().getPopperDOM().classList).toContain( + testClassName, + ); + }); +}); diff --git a/packages/component/__tests__/popup.spec.ts b/packages/component/__tests__/popup.spec.ts new file mode 100644 index 0000000000..54224cd6c3 --- /dev/null +++ b/packages/component/__tests__/popup.spec.ts @@ -0,0 +1,43 @@ +import { TestScene } from '@antv/l7-test-utils'; +import Popup from '../src/popup/popup'; + +describe('popup', () => { + const scene = TestScene(); + const className = 'text-class-popup'; + + it('life cycle', () => { + const popup = new Popup({ + html: '123456', + className: className, + lngLat: { + lng: 120, + lat: 30, + }, + }); + + popup.setOptions({ + lngLat: { lng: 130, lat: 40 }, + }); + + scene.addPopup(popup); + + const targetPopup = document.querySelector(`.${className}`) as HTMLElement; + + expect(targetPopup).not.toBeFalsy(); + expect(popup.getLnglat()).toEqual({ + lng: 130, + lat: 40, + }); + expect(/123456/.test(targetPopup.innerHTML)).toEqual(true); + + expect(targetPopup.classList.contains('l7-popup-hide')).toEqual(false); + + popup.hide(); + + expect(targetPopup.classList.contains('l7-popup-hide')).toEqual(true); + + popup.show(); + + expect(targetPopup.classList.contains('l7-popup-hide')).toEqual(false); + }); +}); diff --git a/packages/component/__tests__/scale.spec.ts b/packages/component/__tests__/scale.spec.ts new file mode 100644 index 0000000000..6bb6c76d4b --- /dev/null +++ b/packages/component/__tests__/scale.spec.ts @@ -0,0 +1,38 @@ +import { TestScene } from '@antv/l7-test-utils'; +import Scale from '../src/control/scale'; + +describe('scale', () => { + const scene = TestScene(); + + it('life cycle', () => { + const scale = new Scale(); + scene.addControl(scale); + + const container = scale.getContainer(); + expect(container.parentElement).toBeInstanceOf(HTMLElement); + + expect( + /\d+\s?km/i.test( + container + .querySelector('.l7-control-scale-line') + ?.innerHTML.toLowerCase() ?? '', + ), + ).toEqual(true); + + scale.setOptions({ + metric: false, + imperial: true, + }); + + expect( + /\d+\s?mi/i.test( + container + .querySelector('.l7-control-scale-line') + ?.innerHTML.toLowerCase() ?? '', + ), + ).toEqual(true); + + scene.removeControl(scale); + expect(container.parentElement).not.toBeInstanceOf(HTMLElement); + }); +}); diff --git a/packages/component/__tests__/selectControl.spec.ts b/packages/component/__tests__/selectControl.spec.ts new file mode 100644 index 0000000000..99846ce98b --- /dev/null +++ b/packages/component/__tests__/selectControl.spec.ts @@ -0,0 +1,105 @@ +import { TestScene } from '@antv/l7-test-utils'; +import SelectControl from '../src/control/baseControl/selectControl'; +import { createL7Icon } from '../src/utils/icon'; + +class SingleControl extends SelectControl { + public getDefault(option: any): any { + return { + ...super.getDefault(option), + options: [ + { + icon: createL7Icon('icon-1'), + label: '1', + value: '1', + }, + { + icon: createL7Icon('icon-2'), + label: '2', + value: '2', + }, + ], + defaultValue: '2', + }; + } + + protected getIsMultiple(): boolean { + return false; + } +} + +class MultiControl extends SelectControl { + public getDefault(option: any): any { + return { + ...super.getDefault(option), + options: [ + { + img: '1', + label: '1', + value: '1', + }, + { + img: '1', + label: '2', + value: '2', + }, + ], + defaultValue: ['2'], + }; + } + protected getIsMultiple(): boolean { + return true; + } +} + +describe('selectControl', () => { + const scene = TestScene(); + + it('life cycle', () => { + const control = new SingleControl({}); + scene.addControl(control); + + const container = control.getContainer(); + expect(container.parentElement).toBeInstanceOf(HTMLElement); + + scene.removeControl(control); + expect(container.parentElement).not.toBeInstanceOf(HTMLElement); + }); + + it('normal single select', () => { + const control = new SingleControl({}); + scene.addControl(control); + + expect(control.getSelectValue()).toEqual('2'); + const popperContainer = control.getPopper().getContent() as HTMLDivElement; + const optionDomList = Array.from( + popperContainer.querySelectorAll('.l7-select-control-item'), + ) as HTMLDivElement[]; + expect(optionDomList).toHaveLength(2); + expect(optionDomList[0].getAttribute('data-option-value')).toEqual('1'); + expect(optionDomList[0].getAttribute('data-option-index')).toEqual('0'); + + optionDomList[0].click(); + + expect(control.getSelectValue()).toEqual('1'); + }); + + it('img multiple select', () => { + const control = new MultiControl({}); + scene.addControl(control); + expect(control.getSelectValue()).toEqual(['2']); + const popperContainer = control.getPopper().getContent() as HTMLDivElement; + const optionDomList = Array.from( + popperContainer.querySelectorAll('.l7-select-control-item'), + ) as HTMLDivElement[]; + expect(optionDomList).toHaveLength(2); + expect(optionDomList[0].getAttribute('data-option-value')).toEqual('1'); + expect(optionDomList[0].getAttribute('data-option-index')).toEqual('0'); + expect(popperContainer.querySelectorAll('img')).toHaveLength(2); + + optionDomList[0].click(); + expect(control.getSelectValue()).toEqual(['2', '1']); + optionDomList[0].click(); + optionDomList[1].click(); + expect(control.getSelectValue()).toEqual([]); + }); +}); diff --git a/packages/component/__tests__/util.spec.ts b/packages/component/__tests__/util.spec.ts new file mode 100644 index 0000000000..ff385833d0 --- /dev/null +++ b/packages/component/__tests__/util.spec.ts @@ -0,0 +1,96 @@ +import { DOM } from '@antv/l7-utils'; +import { createL7Icon } from '../src/utils/icon'; +import { Popper } from '../src/utils/popper'; + +describe('util', () => { + it('icon', () => { + const testClassName = 'l7-test-icon'; + const testIcon = createL7Icon(testClassName); + expect(testIcon).toBeInstanceOf(SVGElement); + expect(testIcon.tagName.toLowerCase()).toEqual('svg'); + const classList = testIcon.classList; + expect(classList).toContain('l7-iconfont'); + }); + + it('popper', () => { + const button = DOM.create('button') as HTMLButtonElement; + button.innerText = 'Test'; + document.body.append(button); + + const testContent = '123456'; + const popper1 = new Popper(button, { + placement: 'left-start', + trigger: 'click', + content: testContent, + className: 'test-popper-class', + container: document.body, + unique: true, + }); + const getPopperClassList = (popper: Popper) => { + return popper.popperDOM.classList; + }; + popper1.show(); + + expect(popper1.getContent()).toEqual(testContent); + expect(getPopperClassList(popper1)).toContain('l7-popper'); + expect(getPopperClassList(popper1)).toContain('test-popper-class'); + expect(getPopperClassList(popper1)).toContain('l7-popper-left'); + expect(getPopperClassList(popper1)).toContain('l7-popper-start'); + expect(getPopperClassList(popper1)).not.toContain('l7-popper-hide'); + popper1.hide(); + + button.click(); + expect(getPopperClassList(popper1)).not.toContain('l7-popper-hide'); + button.click(); + expect(getPopperClassList(popper1)).toContain('l7-popper-hide'); + + const newTestContent = DOM.create('div') as HTMLDivElement; + newTestContent.innerText = '789456'; + popper1.setContent(newTestContent); + expect(popper1.contentDOM.firstChild).toEqual(newTestContent); + popper1.show(); + + const popper2 = new Popper(button, { + placement: 'right-end', + container: document.body, + trigger: 'click', + content: 'hover', + }).show(); + expect(getPopperClassList(popper2)).toContain('l7-popper-end'); + expect(getPopperClassList(popper2)).toContain('l7-popper-right'); + + const popper3 = new Popper(button, { + placement: 'top-start', + container: document.body, + trigger: 'click', + content: 'hover', + }).show(); + expect(getPopperClassList(popper3)).toContain('l7-popper-top'); + expect(getPopperClassList(popper3)).toContain('l7-popper-start'); + + const popper4 = new Popper(button, { + placement: 'bottom-end', + container: document.body, + trigger: 'click', + content: 'hover', + }).show(); + expect(getPopperClassList(popper4)).toContain('l7-popper-bottom'); + expect(getPopperClassList(popper4)).toContain('l7-popper-end'); + + const popper5 = new Popper(button, { + placement: 'left', + container: document.body, + trigger: 'click', + content: 'hover', + }).show(); + expect(getPopperClassList(popper5)).toContain('l7-popper-left'); + + const popper6 = new Popper(button, { + placement: 'top', + container: document.body, + trigger: 'click', + content: 'hover', + }).show(); + expect(getPopperClassList(popper6)).toContain('l7-popper-top'); + }); +}); diff --git a/packages/component/__tests__/zoom.spec.ts b/packages/component/__tests__/zoom.spec.ts new file mode 100644 index 0000000000..431c130dcb --- /dev/null +++ b/packages/component/__tests__/zoom.spec.ts @@ -0,0 +1,34 @@ +import { TestScene } from '@antv/l7-test-utils'; +import Zoom from '../src/control/zoom'; + +describe('zoom', () => { + const scene = TestScene(); + + it('life cycle', () => { + const zoom = new Zoom(); + scene.addControl(zoom); + + const container = zoom.getContainer(); + expect(container.parentElement).toBeInstanceOf(HTMLElement); + + scene.removeControl(zoom); + expect(container.parentElement).not.toBeInstanceOf(HTMLElement); + }); + + it('zoom getDefault', () => { + const zoom = new Zoom(); + scene.addControl(zoom); + + zoom.disable(); + const btnList = Array.from(zoom.getContainer().querySelectorAll('button')); + expect(btnList.map((item) => item.getAttribute('disabled'))).toEqual([ + 'true', + 'true', + ]); + zoom.enable(); + expect(btnList.map((item) => item.getAttribute('disabled'))).toEqual([ + null, + null, + ]); + }); +}); diff --git a/packages/component/package.json b/packages/component/package.json index af14ec3dcd..998d8b0cd8 100644 --- a/packages/component/package.json +++ b/packages/component/package.json @@ -13,6 +13,7 @@ ], "scripts": { "tsc": "tsc --project tsconfig.build.json", + "less": "lessc src/css/index.less src/css/index.css", "clean": "rimraf dist; rimraf es; rimraf lib;", "build": "father build", "build:cjs": "BABEL_ENV=cjs babel src --root-mode upward --out-dir lib --source-maps --extensions .ts,.tsx --delete-dir-on-start --no-comments", @@ -35,7 +36,9 @@ "supercluster": "^7.0.0" }, "devDependencies": { - "@antv/l7-test-utils": "2.9.37" + "@antv/l7-test-utils": "2.9.37", + "gcoord": "^0.3.2", + "less": "^4.1.3" }, "gitHead": "684ba4eb806a798713496d3fc0b4d1e17517dc31", "publishConfig": { diff --git a/packages/component/src/assets/iconfont/iconfont.js b/packages/component/src/assets/iconfont/iconfont.js new file mode 100644 index 0000000000..16d5ff3501 --- /dev/null +++ b/packages/component/src/assets/iconfont/iconfont.js @@ -0,0 +1,73 @@ +(window._iconfont_svg_string_3580659 = + ''), + (function(t) { + try { + var a = (a = document.getElementsByTagName('script'))[a.length - 1], + l = a.getAttribute('data-injectcss'), + a = a.getAttribute('data-disable-injectsvg'); + if (!a) { + var o, + e, + i, + n, + m, + c = function(a, l) { + l.parentNode.insertBefore(a, l); + }; + if (l && !t.__iconfont__svg__cssinject__) { + t.__iconfont__svg__cssinject__ = !0; + try { + document.write( + '', + ); + } catch (a) { + console && console.log(a); + } + } + (o = function() { + var a, + l = document.createElement('div'); + (l.innerHTML = t._iconfont_svg_string_3580659), + (l = l.getElementsByTagName('svg')[0]) && + (l.setAttribute('aria-hidden', 'true'), + (l.style.position = 'absolute'), + (l.style.width = 0), + (l.style.height = 0), + (l.style.overflow = 'hidden'), + (l = l), + (a = document.body).firstChild + ? c(l, a.firstChild) + : a.appendChild(l)); + }), + document.addEventListener + ? ~['complete', 'loaded', 'interactive'].indexOf( + document.readyState, + ) + ? setTimeout(o, 0) + : ((e = function() { + document.removeEventListener('DOMContentLoaded', e, !1), o(); + }), + document.addEventListener('DOMContentLoaded', e, !1)) + : document.attachEvent && + ((i = o), + (n = t.document), + (m = !1), + L(), + (n.onreadystatechange = function() { + 'complete' == n.readyState && + ((n.onreadystatechange = null), d()); + })); + } + function d() { + m || ((m = !0), i()); + } + function L() { + try { + n.documentElement.doScroll('left'); + } catch (a) { + return void setTimeout(L, 50); + } + d(); + } + } catch (e) {} + })(window); diff --git a/packages/component/src/constants/index.ts b/packages/component/src/constants/index.ts new file mode 100644 index 0000000000..c3b0db73d2 --- /dev/null +++ b/packages/component/src/constants/index.ts @@ -0,0 +1,75 @@ +export const GaodeMapStyleConfig = { + normal: { + text: '标准', + img: + 'https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*-nqiT6Vu948AAAAAAAAAAAAAARQnAQ', + }, + light: { + text: '月光银', + img: + 'https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*J_wYQL_PaUEAAAAAAAAAAAAAARQnAQ', + }, + dark: { + text: '幻影黑', + img: + 'https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*U7M9QI1yat4AAAAAAAAAAAAAARQnAQ', + }, + fresh: { + text: '草色青', + img: + 'https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*T-oBT4hB5ucAAAAAAAAAAAAAARQnAQ', + }, + grey: { + text: '雅士灰', + img: + 'https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*OREXQ4vgQRIAAAAAAAAAAAAAARQnAQ', + }, + graffiti: { + text: '涂鸦', + img: + 'https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*4UApTKmeiy4AAAAAAAAAAAAAARQnAQ', + }, + macaron: { + text: '马卡龙', + img: + 'https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*0GrCQLtDjNcAAAAAAAAAAAAAARQnAQ', + }, + darkblue: { + text: '极夜蓝', + img: + 'https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*uWxqSZQlPkkAAAAAAAAAAAAAARQnAQ', + }, + wine: { + text: '酱籽', + img: + 'https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*OFPrTbg3an0AAAAAAAAAAAAAARQnAQ', + }, +}; + +export const MapboxMapStyleConfig = { + normal: { + text: '标准', + img: + 'https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*AnfJTbIBJOkAAAAAAAAAAAAAARQnAQ', + }, + light: { + text: '亮', + img: + 'https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*gnuiQIok9qIAAAAAAAAAAAAAARQnAQ', + }, + dark: { + text: '暗', + img: + 'https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*NwG-TbOlBH0AAAAAAAAAAAAAARQnAQ', + }, + satellite: { + text: '卫星', + img: + 'https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*2X5EQLKul3IAAAAAAAAAAAAAARQnAQ', + }, + outdoors: { + text: '户外', + img: + 'https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*gXFLRIaBUI0AAAAAAAAAAAAAARQnAQ', + }, +}; diff --git a/packages/component/src/control/BaseControl.ts b/packages/component/src/control/BaseControl.ts deleted file mode 100644 index d39ce7d1b3..0000000000 --- a/packages/component/src/control/BaseControl.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { - IControlOption, - IControlService, - ILayerService, - IMapService, - IRendererService, - PositionType, - TYPES, -} from '@antv/l7-core'; -import { DOM } from '@antv/l7-utils'; -import { EventEmitter } from 'eventemitter3'; -import { Container } from 'inversify'; - -export { PositionType } from '@antv/l7-core'; - -let controlId = 0; -export default class Control extends EventEmitter { - public controlOption: IControlOption; - protected container: HTMLElement; - protected sceneContainer: Container; - protected mapsService: IMapService; - protected renderService: IRendererService; - protected layerService: ILayerService; - protected controlService: IControlService; - - private isShow: boolean; - - constructor(cfg?: Partial) { - super(); - this.controlOption = { - ...this.getDefault(), - ...(cfg || {}), - }; - } - - public getDefault() { - return { - position: PositionType.TOPRIGHT, - name: `${controlId++}`, - }; - } - - public setPosition(position: PositionType = PositionType.BOTTOMRIGHT) { - // 考虑组件的自动布局,需要销毁重建 - const controlService = this.controlService; - if (controlService) { - controlService.removeControl(this); - } - this.controlOption.position = position; - if (controlService) { - controlService.addControl(this, this.sceneContainer); - } - return this; - } - public addTo(sceneContainer: Container) { - this.mapsService = sceneContainer.get(TYPES.IMapService); - this.renderService = sceneContainer.get( - TYPES.IRendererService, - ); - this.layerService = sceneContainer.get(TYPES.ILayerService); - this.controlService = sceneContainer.get( - TYPES.IControlService, - ); - this.sceneContainer = sceneContainer; - this.isShow = true; - this.container = this.onAdd(); - const container = this.container; - const pos = this.controlOption.position; - const corner = this.controlService.controlCorners[pos]; - DOM.addClass(container, 'l7-control'); - - if (pos.indexOf('bottom') !== -1) { - corner.insertBefore(container, corner.firstChild); - } else { - corner.appendChild(container); - } - return this; - } - public onAdd(): HTMLElement { - throw new Error('Method not implemented.'); - } - - public onRemove(): void { - throw new Error('Method not implemented.'); - } - public hide() { - const container = this.container; - DOM.addClass(container, 'l7-control-hide'); - this.isShow = false; - } - public show() { - const container = this.container; - DOM.removeClass(container, 'l7-control-hide'); - this.isShow = true; - } - public remove() { - if (!this.mapsService) { - return this; - } - DOM.remove(this.container); - this.onRemove(); - } - public _refocusOnMap(e: MouseEvent) { - // if map exists and event is not a keyboard event - if (this.mapsService && e && e.screenX > 0 && e.screenY > 0) { - const container = this.mapsService.getContainer(); - if (container !== null) { - container.focus(); - } - } - } -} diff --git a/packages/component/src/control/__tests__/BaseControl.spec.ts b/packages/component/src/control/__tests__/BaseControl.spec.ts deleted file mode 100644 index f4efc459d0..0000000000 --- a/packages/component/src/control/__tests__/BaseControl.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -describe('BaseControl', () => { - // const el = document.createElement('div'); - // el.id = 'test-div-id'; - // el.style.width = '500px'; - // el.style.height = '500px'; - // el.style.position = 'absolute'; - // document.querySelector('body')?.appendChild(el); - // const scene = new Scene({ - // id: 'test-div-id', - // map: new Map({ - // style: 'dark', - // center: [110.19382669582967, 30.258134], - // pitch: 0, - // zoom: 3, - // }), - // }); - it('control', () => { - expect(1).toEqual(1) - }); - - - }); - \ No newline at end of file diff --git a/packages/component/src/control/__tests__/zoom.spec.ts b/packages/component/src/control/__tests__/zoom.spec.ts deleted file mode 100644 index 0cd05cff56..0000000000 --- a/packages/component/src/control/__tests__/zoom.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -import Zoom from '../zoom'; -import { TestScene } from '@antv/l7-test-utils' - - -describe('zoom', () => { - const zoom = new Zoom() - it('zoom getDefault', () => { - expect(zoom.getDefault().name).toEqual('zoom'); - const scene = TestScene(); - scene.addControl(zoom); - zoom.disable(); - - }); - - - }); - \ No newline at end of file diff --git a/packages/component/src/control/baseControl/buttonControl.ts b/packages/component/src/control/baseControl/buttonControl.ts new file mode 100644 index 0000000000..ad9b47a770 --- /dev/null +++ b/packages/component/src/control/baseControl/buttonControl.ts @@ -0,0 +1,150 @@ +import { DOM } from '@antv/l7-utils'; +import { ELType } from '@antv/l7-utils/src/dom'; +import Control, { IControlOption } from './control'; + +export { ButtonControl }; + +export interface IButtonControlOption extends IControlOption { + btnIcon?: ELType | DocumentFragment; + btnText?: string; + title?: string; + vertical?: boolean; +} + +export default class ButtonControl< + O extends IButtonControlOption = IButtonControlOption +> extends Control { + /** + * 当前按钮是否禁用 + * @protected + */ + protected isDisable = false; + + /** + * 按钮的 DOM + * @protected + */ + protected button?: HTMLElement; + + /** + * 按钮中文本对应的 DOM + * @protected + */ + protected buttonText?: HTMLElement; + + /** + * 按钮中图标对应的 DOM + * @protected + */ + protected buttonIcon?: ELType | DocumentFragment; + + /** + * 设置当前按钮 + * @param newIsDisable + */ + public setIsDisable(newIsDisable: boolean) { + this.isDisable = newIsDisable; + if (newIsDisable) { + this.button?.setAttribute('disabled', 'true'); + } else { + this.button?.removeAttribute('disabled'); + } + } + + public createButton(className: string = '') { + return DOM.create( + 'button', + `l7-button-control ${className}`, + ) as HTMLElement; + } + + public onAdd(): HTMLElement { + this.button = this.createButton(); + this.isDisable = false; + const { title, btnText, btnIcon } = this.controlOption; + this.setBtnTitle(title); + this.setBtnText(btnText); + this.setBtnIcon(btnIcon); + return this.button; + } + + public onRemove(): void { + this.button = this.buttonIcon = this.buttonText = undefined; + this.isDisable = false; + } + + /** + * 更新配置方法 + * @param newOptions + */ + public setOptions(newOptions: Partial) { + const { title, btnText, btnIcon } = newOptions; + if (this.checkUpdateOption(newOptions, ['title'])) { + this.setBtnTitle(title); + } + if (this.checkUpdateOption(newOptions, ['btnIcon'])) { + this.setBtnIcon(btnIcon); + } + if (this.checkUpdateOption(newOptions, ['btnText'])) { + this.setBtnText(btnText); + } + super.setOptions(newOptions); + } + + /** + * 设置按钮 title + * @param title + */ + public setBtnTitle(title: O['title']) { + this.button?.setAttribute('title', title ?? ''); + } + + /** + * 设置按钮 Icon + * @param newIcon + */ + public setBtnIcon(newIcon: O['btnIcon']) { + if (this.buttonIcon) { + DOM.remove(this.buttonIcon); + } + if (newIcon) { + const firstChild = this.button?.firstChild; + if (firstChild) { + this.button?.insertBefore(newIcon, firstChild); + } else { + this.button?.appendChild(newIcon); + } + this.buttonIcon = newIcon; + } + } + + /** + * 设置按钮文本 + * @param newText + */ + public setBtnText(newText: O['btnText']) { + if (!this.button) { + return; + } + DOM.removeClass(this.button, 'l7-button-control--row'); + DOM.removeClass(this.button, 'l7-button-control--column'); + if (newText) { + let btnText = this.buttonText; + if (!btnText) { + btnText = DOM.create('div', 'l7-button-control__text') as HTMLElement; + this.button?.appendChild(btnText); + this.buttonText = btnText; + } + btnText.innerText = newText; + DOM.addClass( + this.button, + this.controlOption.vertical + ? 'l7-button-control--column' + : 'l7-button-control--row', + ); + } else if (!newText && this.buttonText) { + DOM.remove(this.buttonText); + this.buttonText = undefined; + } + } +} diff --git a/packages/component/src/control/baseControl/control.ts b/packages/component/src/control/baseControl/control.ts new file mode 100644 index 0000000000..ac20c94587 --- /dev/null +++ b/packages/component/src/control/baseControl/control.ts @@ -0,0 +1,295 @@ +import { + IControl, + IControlService, + IGlobalConfigService, + ILayerService, + IMapService, + IRendererService, + ISceneService, + PositionName, + PositionType, + TYPES, +} from '@antv/l7-core'; +import { DOM } from '@antv/l7-utils'; +import EventEmitter from 'eventemitter3'; +import { Container } from 'inversify'; +import { ControlEvent } from '../../interface'; + +export { PositionType } from '@antv/l7-core'; + +export { Control }; + +export interface IControlOption { + name: string; + position: PositionName; + className?: string; + style?: string; + [key: string]: any; +} + +export default class Control + extends EventEmitter + implements IControl { + /** + * 当前类型控件实例个数 + * @protected + */ + protected static controlCount = 0; + + /** + * 当前控件实例配置 + */ + public controlOption: O; + + /** + * 控件的 DOM 容器 + * @protected + */ + protected container: HTMLElement; + + /** + * 当前控件是否显示 + * @protected + */ + protected isShow: boolean; + + protected sceneContainer: Container; + protected scene: ISceneService; + protected mapsService: IMapService; + protected renderService: IRendererService; + protected layerService: ILayerService; + protected controlService: IControlService; + protected configService: IGlobalConfigService; + + constructor(option?: Partial) { + super(); + Control.controlCount++; + this.controlOption = { + ...this.getDefault(option), + ...(option || {}), + }; + } + + public getOptions() { + return this.controlOption; + } + + /** + * 更新配置的方法,子类如果有自己的配置,也需要重写该方法 + * @param newOptions + */ + public setOptions(newOptions: Partial): void { + const defaultOptions = this.getDefault(newOptions); + (Object.entries(newOptions) as Array<[keyof O, any]>).forEach( + ([key, value]) => { + if (value === undefined) { + newOptions[key] = defaultOptions[key]; + } + }, + ); + if ('position' in newOptions) { + this.setPosition(newOptions.position); + } + if ('className' in newOptions) { + this.setClassName(newOptions.className); + } + if ('style' in newOptions) { + this.setStyle(newOptions.style); + } + this.controlOption = { + ...this.controlOption, + ...newOptions, + }; + } + + /** + * 当 Control 被添加至 Scene 中,被 controlService 调用的方法 + * @param sceneContainer + */ + public addTo(sceneContainer: Container) { + // 初始化各个 Service 实例 + this.mapsService = sceneContainer.get(TYPES.IMapService); + this.renderService = sceneContainer.get( + TYPES.IRendererService, + ); + this.layerService = sceneContainer.get(TYPES.ILayerService); + this.controlService = sceneContainer.get( + TYPES.IControlService, + ); + this.configService = sceneContainer.get( + TYPES.IGlobalConfigService, + ); + this.scene = sceneContainer.get(TYPES.ISceneService); + this.sceneContainer = sceneContainer; + this.isShow = true; + + // 初始化 container + this.container = this.onAdd(); + DOM.addClass(this.container, 'l7-control'); + + const { className, style } = this.controlOption; + if (className) { + this.setClassName(className); + } + if (style) { + this.setStyle(style); + } + // 将 container 插入容器中 + this.insertContainer(); + this.emit('add', this); + return this; + } + + /** + * 将控件移除时触发 + */ + public remove() { + if (!this.mapsService) { + return this; + } + DOM.remove(this.container); + this.onRemove(); + this.emit('remove', this); + } + + /** + * Control 被添加的时候被调用,返回 Control 对应的 DOM 容器 + */ + public onAdd(): HTMLElement { + return DOM.create('div'); + } + + /** + * Control 被移除时调用 + */ + // tslint:disable-next-line:no-empty + public onRemove() {} + + /** + * 显示控件时触发 + */ + public show() { + const container = this.container; + DOM.removeClass(container, 'l7-control--hide'); + this.isShow = true; + this.emit('show', this); + } + + /** + * 隐藏控件时触发 + */ + public hide() { + const container = this.container; + DOM.addClass(container, 'l7-control--hide'); + this.isShow = false; + this.emit('hide', this); + } + + /** + * 获取默认构造器参数 + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public getDefault(option?: Partial): O { + // tslint:disable-next-line:no-object-literal-type-assertion + return { + position: PositionType.TOPRIGHT, + name: `${Control.controlCount}`, + } as O; + } + + /** + * 获取当前控件对应的 DOM 容器 + */ + public getContainer() { + return this.container; + } + + /** + * 获取当前 Control 是否展示 + */ + public getIsShow() { + return this.isShow; + } + + public _refocusOnMap(e: MouseEvent) { + // if map exists and event is not a keyboard event + if (this.mapsService && e && e.screenX > 0 && e.screenY > 0) { + const container = this.mapsService.getContainer(); + if (container !== null) { + container.focus(); + } + } + } + + /** + * 设置当前控件位置 + * @param position + */ + public setPosition( + position: PositionType | PositionName = PositionType.TOPLEFT, + ) { + // 考虑组件的自动布局,需要销毁重建 + const controlService = this.controlService; + if (controlService) { + controlService.removeControl(this); + } + this.controlOption.position = position; + if (controlService) { + controlService.addControl(this, this.sceneContainer); + } + return this; + } + + /** + * 设置容器 container 的样式相关位置,包含 className + * @param className + */ + public setClassName(className?: string | null) { + const container = this.container; + const { className: oldClassName } = this.controlOption; + if (oldClassName) { + DOM.removeClass(container, oldClassName); + } + if (className) { + DOM.addClass(container, className); + } + } + + /** + * 设置容器 container 的样式相关位置,包含 style + * @param style + */ + public setStyle(style?: string | null) { + const container = this.container; + if (style) { + container.setAttribute('style', style); + } else { + container.removeAttribute('style'); + } + } + + /** + * 将控件 DOM 插入到对应 position 的容器中 + * @protected + */ + protected insertContainer() { + const container = this.container; + const position = this.controlOption.position; + const corner = this.controlService.controlCorners[position]; + if (position.indexOf('bottom') !== -1) { + corner.insertBefore(container, corner.firstChild); + } else { + corner.appendChild(container); + } + } + + /** + * 检查当前传入 option 是否包含 keys 字段 + * @param option + * @param keys + * @protected + */ + protected checkUpdateOption(option: Partial, keys: Array) { + return keys.some((key) => key in option); + } +} diff --git a/packages/component/src/control/baseControl/index.ts b/packages/component/src/control/baseControl/index.ts new file mode 100644 index 0000000000..c0c4b2fcda --- /dev/null +++ b/packages/component/src/control/baseControl/index.ts @@ -0,0 +1,4 @@ +export * from './control'; +export * from './buttonControl'; +export * from './popperControl'; +export * from './selectControl'; diff --git a/packages/component/src/control/baseControl/popperControl.ts b/packages/component/src/control/baseControl/popperControl.ts new file mode 100644 index 0000000000..e492377c50 --- /dev/null +++ b/packages/component/src/control/baseControl/popperControl.ts @@ -0,0 +1,111 @@ +import { PositionName } from '@antv/l7-core'; +import { Popper, PopperPlacement, PopperTrigger } from '../../utils/popper'; +import ButtonControl, { IButtonControlOption } from './buttonControl'; + +export { PopperControl }; + +export interface IPopperControlOption extends IButtonControlOption { + popperPlacement: PopperPlacement; + popperClassName?: string; + popperTrigger: PopperTrigger; +} + +const PopperPlacementMap: Record = { + topleft: 'right-start', + topcenter: 'bottom', + topright: 'left-start', + bottomleft: 'right-end', + bottomcenter: 'top', + bottomright: 'left-end', + lefttop: 'bottom-start', + leftcenter: 'right', + leftbottom: 'top-start', + righttop: 'bottom-end', + rightcenter: 'left', + rightbottom: 'top-end', +}; + +export default class PopperControl< + O extends IPopperControlOption = IPopperControlOption +> extends ButtonControl { + /** + * 气泡实例 + * @protected + */ + protected popper!: Popper; + + public getPopper() { + return this.popper; + } + + public hide() { + this.popper.hide(); + super.hide(); + } + + /** + * 获取默认配置 + * @param option + */ + public getDefault(option?: Partial): O { + const defaultOption = super.getDefault(option); + const position = option?.position ?? defaultOption.position!; + return { + ...super.getDefault(option), + popperPlacement: PopperPlacementMap[position], + popperTrigger: 'click', + }; + } + + public onAdd(): HTMLElement { + const button = super.onAdd(); + this.initPopper(); + return button; + } + + public onRemove() { + this.popper.destroy(); + } + + public initPopper() { + const { + popperClassName, + popperPlacement, + popperTrigger, + } = this.controlOption; + const popperContainer = this.mapsService.getMapContainer()!; + + this.popper = new Popper(this.button!, { + className: popperClassName, + placement: popperPlacement, + trigger: popperTrigger, + container: popperContainer, + unique: true, + }); + this.popper + .on('show', () => { + this.emit('popperShow', this); + }) + .on('hide', () => { + this.emit('popperHide', this); + }); + return this.popper; + } + + public setOptions(option: Partial) { + super.setOptions(option); + + if ( + this.checkUpdateOption(option, [ + 'popperPlacement', + 'popperTrigger', + 'popperClassName', + ]) + ) { + const content = this.popper.getContent(); + this.popper.destroy(); + this.initPopper(); + this.popper.setContent(content); + } + } +} diff --git a/packages/component/src/control/baseControl/selectControl.ts b/packages/component/src/control/baseControl/selectControl.ts new file mode 100644 index 0000000000..d6503fa9f8 --- /dev/null +++ b/packages/component/src/control/baseControl/selectControl.ts @@ -0,0 +1,220 @@ +import { DOM } from '@antv/l7-utils'; +import { IPopperControlOption, PopperControl } from './popperControl'; + +type BaseOptionItem = { + value: string; + text: string; + [key: string]: string; +}; + +type NormalOptionItem = BaseOptionItem & { + icon?: HTMLElement; +}; + +type ImageOptionItem = BaseOptionItem & { + img: string; +}; + +export type ControlOptionItem = ImageOptionItem | NormalOptionItem; + +export interface ISelectControlOption extends IPopperControlOption { + options: ControlOptionItem[]; + defaultValue?: string | string[]; +} + +export { SelectControl }; + +enum SelectControlConstant { + ActiveOptionClassName = 'l7-select-control-item-active', + OptionValueAttrKey = 'data-option-value', + OptionIndexAttrKey = 'data-option-index', +} + +export default class SelectControl< + O extends ISelectControlOption = ISelectControlOption +> extends PopperControl { + /** + * 当前选中的值 + * @protected + */ + protected selectValue: string[] = []; + + /** + * 选项对应的 DOM 列表 + * @protected + */ + protected optionDOMList: HTMLElement[]; + + public setOptions(option: Partial) { + super.setOptions(option); + const { options } = option; + if (options) { + this.popper.setContent(this.getPopperContent(options)); + } + } + + public onAdd() { + const button = super.onAdd(); + const { defaultValue } = this.controlOption; + if (defaultValue) { + this.selectValue = this.transSelectValue(defaultValue); + } + this.popper.setContent(this.getPopperContent(this.controlOption.options)); + return button; + } + + public getSelectValue() { + return this.getIsMultiple() ? this.selectValue : this.selectValue[0]; + } + + public setSelectValue(value: string | string[], emitEvent = true) { + const finalValue = this.transSelectValue(value); + this.optionDOMList.forEach((optionDOM) => { + const optionValue = optionDOM.getAttribute( + SelectControlConstant.OptionValueAttrKey, + )!; + const checkboxDOM = this.getIsMultiple() + ? optionDOM.querySelector('input[type=checkbox]') + : undefined; + if (finalValue.includes(optionValue)) { + DOM.addClass(optionDOM, SelectControlConstant.ActiveOptionClassName); + if (checkboxDOM) { + // @ts-ignore + DOM.setChecked(checkboxDOM, true); + } + } else { + DOM.removeClass(optionDOM, SelectControlConstant.ActiveOptionClassName); + if (checkboxDOM) { + // @ts-ignore + DOM.setChecked(checkboxDOM, false); + } + } + }); + this.selectValue = finalValue; + if (emitEvent) { + this.emit( + 'selectChange', + this.getIsMultiple() ? finalValue : finalValue[0], + ); + } + } + + /** + * 是否为多选 + * @protected + */ + protected getIsMultiple() { + return false; + } + + protected getPopperContent(options: ControlOptionItem[]): HTMLElement { + const isImageOptions = this.isImageOptions(); + const content = DOM.create( + 'div', + isImageOptions ? 'l7-select-control--image' : 'l7-select-control--normal', + ) as HTMLElement; + if (this.getIsMultiple()) { + DOM.addClass(content, 'l7-select-control--multiple'); + } + const optionsDOMList = options.map((option, optionIndex) => { + const optionDOM = isImageOptions + ? // @ts-ignore + this.createImageOption(option) + : this.createNormalOption(option); + + optionDOM.setAttribute( + SelectControlConstant.OptionValueAttrKey, + option.value, + ); + optionDOM.setAttribute( + SelectControlConstant.OptionIndexAttrKey, + window.String(optionIndex), + ); + optionDOM.addEventListener('click', this.onItemClick.bind(this, option)); + return optionDOM; + }); + content.append(...optionsDOMList); + this.optionDOMList = optionsDOMList; + return content; + } + + protected createNormalOption = (option: NormalOptionItem) => { + const isSelect = this.selectValue.includes(option.value); + const optionDOM = DOM.create( + 'div', + `l7-select-control-item ${ + isSelect ? SelectControlConstant.ActiveOptionClassName : '' + }`, + ) as HTMLElement; + if (this.getIsMultiple()) { + optionDOM.appendChild(this.createCheckbox(isSelect)); + } + if (option.icon) { + optionDOM.appendChild(option.icon); + } + const textDOM = DOM.create('span'); + textDOM.innerText = option.text; + optionDOM.appendChild(textDOM); + return optionDOM; + }; + + protected createImageOption(option: ImageOptionItem): HTMLElement { + const isSelect = this.selectValue.includes(option.value); + const optionDOM = DOM.create( + 'div', + `l7-select-control-item ${ + isSelect ? SelectControlConstant.ActiveOptionClassName : '' + }`, + ) as HTMLElement; + const imgDOM = DOM.create('img') as HTMLElement; + imgDOM.setAttribute('src', option.img); + DOM.setUnDraggable(imgDOM); + optionDOM.appendChild(imgDOM); + const rowDOM = DOM.create( + 'div', + 'l7-select-control-item-row', + ) as HTMLElement; + if (this.getIsMultiple()) { + optionDOM.appendChild(this.createCheckbox(isSelect)); + } + const textDOM = DOM.create('span'); + textDOM.innerText = option.text; + rowDOM.appendChild(textDOM); + optionDOM.appendChild(rowDOM); + return optionDOM; + } + + protected createCheckbox(isSelect: boolean) { + const checkboxDOM = DOM.create('input') as HTMLElement; + checkboxDOM.setAttribute('type', 'checkbox'); + if (isSelect) { + DOM.setChecked(checkboxDOM, true); + } + return checkboxDOM; + } + + protected onItemClick = (item: ControlOptionItem) => { + if (this.getIsMultiple()) { + const targetIndex = this.selectValue.findIndex( + (value) => value === item.value, + ); + if (targetIndex > -1) { + this.selectValue.splice(targetIndex, 1); + } else { + this.selectValue = [...this.selectValue, item.value]; + } + } else { + this.selectValue = [item.value]; + } + this.setSelectValue(this.selectValue); + }; + + protected isImageOptions() { + // @ts-ignore + return !!this.controlOption.options.find((item) => item.img); + } + + protected transSelectValue(value: string | string[]) { + return Array.isArray(value) ? value : [value]; + } +} diff --git a/packages/component/src/control/exportImage.ts b/packages/component/src/control/exportImage.ts new file mode 100644 index 0000000000..73d7046751 --- /dev/null +++ b/packages/component/src/control/exportImage.ts @@ -0,0 +1,73 @@ +import { createL7Icon } from '../utils/icon'; +import ButtonControl, { + IButtonControlOption, +} from './baseControl/buttonControl'; + +export interface IExportImageControlOption extends IButtonControlOption { + imageType: 'png' | 'jpeg'; + onExport: (base64: string) => void; +} + +export { ExportImage }; + +export default class ExportImage extends ButtonControl< + IExportImageControlOption +> { + public onAdd(): HTMLElement { + const button = super.onAdd(); + button.addEventListener('click', this.onClick); + return button; + } + + public getDefault( + option?: Partial, + ): IExportImageControlOption { + return { + ...super.getDefault(option), + title: '导出图片', + btnIcon: createL7Icon('l7-icon-export-picture'), + imageType: 'png', + }; + } + + public getImage() { + const mapImage = this.mapsService.exportMap('png'); + const layerImage = this.scene.exportPng('png'); + return this.mergeImage(mapImage, layerImage); + } + + protected onClick = async () => { + const { onExport } = this.controlOption; + onExport?.(await this.getImage()); + }; + + /** + * 将多张图片合并为一张图片 + * @protected + * @param base64List + */ + protected mergeImage = async (...base64List: string[]) => { + const { imageType } = this.controlOption; + const { width = 0, height = 0 } = + this.mapsService.getContainer()?.getBoundingClientRect() ?? {}; + const canvas = document.createElement('canvas') as HTMLCanvasElement; + canvas.width = width; + canvas.height = height; + const context = canvas.getContext('2d'); + const imgList = await Promise.all( + base64List.map((base64) => { + return new Promise((resolve) => { + const img = new Image(); + img.onload = () => { + resolve(img); + }; + img.src = base64; + }); + }), + ); + imgList.forEach((img) => { + context?.drawImage(img, 0, 0, width, height); + }); + return canvas.toDataURL(`image/${imageType}`) as string; + }; +} diff --git a/packages/component/src/control/fullscreen.ts b/packages/component/src/control/fullscreen.ts new file mode 100644 index 0000000000..d76c5a8ec4 --- /dev/null +++ b/packages/component/src/control/fullscreen.ts @@ -0,0 +1,111 @@ +import { DOM } from '@antv/l7-utils'; +import { createL7Icon } from '../utils/icon'; +import ScreenFull from '../utils/screenfull'; +import ButtonControl, { + IButtonControlOption, +} from './baseControl/buttonControl'; + +export interface IFullscreenControlOption extends IButtonControlOption { + exitBtnText: IButtonControlOption['btnText']; + exitBtnIcon: IButtonControlOption['btnIcon']; + exitTitle: IButtonControlOption['title']; +} + +export { Fullscreen }; + +export default class Fullscreen extends ButtonControl< + IFullscreenControlOption +> { + protected isFullscreen = false; + + protected mapContainer: HTMLElement; + + constructor(option?: Partial) { + super(option); + + if (!ScreenFull.isEnabled) { + console.warn('当前浏览器环境不支持对地图全屏化'); + } + } + + public setOptions(newOptions: Partial) { + const { exitBtnText, exitBtnIcon, exitTitle } = newOptions; + if (this.isFullscreen) { + if (this.checkUpdateOption(newOptions, ['exitBtnIcon'])) { + this.setBtnIcon(exitBtnIcon); + } + if (this.checkUpdateOption(newOptions, ['exitBtnText'])) { + this.setBtnText(exitBtnText); + } + if (this.checkUpdateOption(newOptions, ['exitTitle'])) { + this.setBtnTitle(exitTitle); + } + } + super.setOptions(newOptions); + } + + public onAdd(): HTMLElement { + const button = super.onAdd(); + button.addEventListener('click', this.onClick); + this.mapContainer = DOM.getContainer(this.scene.getSceneConfig().id!); + this.mapContainer.addEventListener( + 'fullscreenchange', + this.onFullscreenChange, + ); + return button; + } + + public onRemove() { + super.onRemove(); + this.mapContainer.removeEventListener( + 'fullscreenchange', + this.onFullscreenChange, + ); + } + + public getDefault( + option?: Partial, + ): IFullscreenControlOption { + return { + ...super.getDefault(option), + title: '全屏', + btnIcon: createL7Icon('l7-icon-fullscreen'), + exitTitle: '退出全屏', + exitBtnIcon: createL7Icon('l7-icon-exit-fullscreen'), + }; + } + + public toggleFullscreen = async () => { + if (ScreenFull.isEnabled) { + await ScreenFull.toggle(this.mapContainer); + } + }; + + protected onClick = () => { + this.toggleFullscreen(); + }; + + protected onFullscreenChange = () => { + this.isFullscreen = !!document.fullscreenElement; + + const { + btnText, + btnIcon, + title, + exitBtnText, + exitBtnIcon, + exitTitle, + } = this.controlOption; + if (this.isFullscreen) { + this.setBtnTitle(exitTitle); + this.setBtnText(exitBtnText); + this.setBtnIcon(exitBtnIcon); + } else { + this.setBtnTitle(title); + this.setBtnText(btnText); + this.setBtnIcon(btnIcon); + } + + this.emit('fullscreenChange', this.isFullscreen); + }; +} diff --git a/packages/component/src/control/geoLocate.ts b/packages/component/src/control/geoLocate.ts new file mode 100644 index 0000000000..1f90c179f4 --- /dev/null +++ b/packages/component/src/control/geoLocate.ts @@ -0,0 +1,70 @@ +import { Point } from '@antv/l7-core'; +import { isNaN } from 'lodash'; +import { createL7Icon } from '../utils/icon'; +import ButtonControl, { + IButtonControlOption, +} from './baseControl/buttonControl'; + +export interface IGeoLocateOption extends IButtonControlOption { + transform: (position: Point) => Point | Promise; +} + +export { GeoLocate }; + +export default class GeoLocate extends ButtonControl { + constructor(option?: Partial) { + super(option); + + if (!window.navigator.geolocation) { + console.warn('当前浏览器环境不支持获取地理定位'); + } + } + + public getDefault(option?: Partial): IGeoLocateOption { + return { + ...super.getDefault(option), + title: '定位', + btnIcon: createL7Icon('l7-icon-reposition'), + }; + } + + public onAdd(): HTMLElement { + const button = super.onAdd(); + button.addEventListener('click', this.onClick); + return button; + } + + /** + * 通过浏览器 API 获取当前所在经纬度 + */ + public getGeoLocation = () => { + return new Promise((resolve, reject) => { + window.navigator.geolocation.getCurrentPosition( + ({ coords }) => { + const { longitude, latitude } = coords ?? {}; + if (!isNaN(longitude) && !isNaN(latitude)) { + resolve([longitude, latitude]); + } else { + reject(); + } + }, + (e) => { + reject(e); + }, + ); + }); + }; + + public onClick = async () => { + if (!window.navigator.geolocation) { + return; + } + const { transform } = this.controlOption; + const position = await this.getGeoLocation(); + const currentZoom = this.mapsService.getZoom(); + this.mapsService.setZoomAndCenter( + currentZoom > 15 ? currentZoom : 15, + transform ? await transform(position) : position, + ); + }; +} diff --git a/packages/component/src/control/layer.ts b/packages/component/src/control/layer.ts deleted file mode 100644 index 9c088bb13a..0000000000 --- a/packages/component/src/control/layer.ts +++ /dev/null @@ -1,317 +0,0 @@ -import { PositionType } from '@antv/l7-core'; -import { bindAll, DOM } from '@antv/l7-utils'; -import { ILayerControlOption } from '../interface'; -import Control from './BaseControl'; - -interface IInputItem extends HTMLInputElement { - layerId: string; -} - -export default class Layers extends Control { - private layerControlInputs: any[]; - private layers: any[]; - private lastZIndex: number; - private handlingClick: boolean; - private layersLink: HTMLElement; - private baseLayersList: HTMLElement; - private separator: HTMLElement; - private overlaysList: HTMLElement; - private form: HTMLElement; - - constructor(cfg: Partial) { - super(cfg); - this.layerControlInputs = []; - this.layers = []; - this.lastZIndex = 0; - this.handlingClick = false; - this.initLayers(); - - bindAll( - [ - 'checkDisabledLayers', - 'onLayerChange', - 'collapse', - 'extend', - 'expand', - 'onInputClick', - ], - this, - ); - } - - public getDefault() { - return { - collapsed: true, - position: PositionType.TOPRIGHT, - autoZIndex: true, - hideSingleBase: false, - sortLayers: false, - name: 'layers', - }; - } - public onAdd() { - this.initLayout(); - this.update(); - this.mapsService.on('zoomend', this.checkDisabledLayers); - this.layers.forEach((layerItem) => { - layerItem.layer.on('remove', this.onLayerChange); - layerItem.layer.on('add', this.onLayerChange); - }); - return this.container; - } - - public addVisualLayer(layer: any, name: string | number) { - this.addLayer(layer, name, true); - return this.mapsService ? this.update() : this; - } - public expand() { - const { height } = this.renderService.getViewportSize(); - DOM.addClass(this.container, 'l7-control-layers-expanded'); - this.form.style.height = 'null'; - const acceptableHeight = height - (this.container.offsetTop + 50); - if (acceptableHeight < this.form.clientHeight) { - DOM.addClass(this.form, 'l7-control-layers-scrollbar'); - this.form.style.height = acceptableHeight + 'px'; - } else { - DOM.removeClass(this.form, 'l7-control-layers-scrollbar'); - } - this.checkDisabledLayers(); - return this; - } - - public collapse() { - DOM.removeClass(this.container, 'l7-control-layers-expanded'); - return this; - } - - public onRemove() { - if (!this.mapsService) { - return; - } - this.mapsService.off('click', this.collapse); - this.layers.forEach((layerItem) => { - layerItem.layer.off('remove', this.onLayerChange); - layerItem.layer.off('add', this.onLayerChange); - }); - } - private initLayout() { - const className = 'l7-control-layers'; - const container = (this.container = DOM.create('div', className)); - const { collapsed } = this.controlOption; - // makes this work on IE touch devices by stopping it from firing a mouseout event when the touch is released - container.setAttribute('aria-haspopup', 'true'); - const form = (this.form = DOM.create( - 'form', - className + '-list', - ) as HTMLElement); - - if (collapsed) { - this.mapsService.on('click', this.collapse); - container.addEventListener('mouseenter', this.expand); - container.addEventListener('mouseleave', this.collapse); - } - - this.layersLink = DOM.create('a', className + '-toggle', container); - const link = this.layersLink; - // link.href = '#'; - link.title = 'Layers'; - if (!collapsed) { - this.expand(); - } - this.baseLayersList = DOM.create('div', className + '-base', form); - this.separator = DOM.create('div', className + '-separator', form); - this.overlaysList = DOM.create('div', className + '-overlays', form); - container.appendChild(form); - } - private initLayers() { - const { baseLayers = {}, overlayers = {} } = this.controlOption; - Object.keys(baseLayers).forEach((name: string) => { - // baseLayers[name].once('inited', this.update); - this.addLayer(baseLayers[name], name, false); - }); - Object.keys(overlayers).forEach((name: any) => { - // overlayers[name].once('inited', this.update); - this.addLayer(overlayers[name], name, true); - }); - } - - private update() { - if (!this.container) { - return this; - } - - DOM.empty(this.baseLayersList); - DOM.empty(this.overlaysList); - - this.layerControlInputs = []; - let baseLayersPresent; - let overlaysPresent; - let i; - let obj; - let baseLayersCount = 0; - - for (i = 0; i < this.layers.length; i++) { - obj = this.layers[i]; - this.addItem(obj); - overlaysPresent = overlaysPresent || obj.overlay; - baseLayersPresent = baseLayersPresent || !obj.overlay; - baseLayersCount += !obj.overlay ? 1 : 0; - } - - // Hide base layers section if there's only one layer. - if (this.controlOption.hideSingleBase) { - baseLayersPresent = baseLayersPresent && baseLayersCount > 1; - this.baseLayersList.style.display = baseLayersPresent ? '' : 'none'; - } - - this.separator.style.display = - overlaysPresent && baseLayersPresent ? '' : 'none'; - - return this; - } - - private checkDisabledLayers() { - const inputs = this.layerControlInputs; - let input: IInputItem; - let layer; - const zoom = this.mapsService.getZoom(); - - for (let i = inputs.length - 1; i >= 0; i--) { - input = inputs[i]; - layer = this.layerService.getLayer(input.layerId); - - if (layer && layer.inited) { - const minZoom = layer.getMinZoom(); - const maxZoom = layer.getMaxZoom(); - - input.disabled = zoom < minZoom || zoom > maxZoom; - } - } - } - - private addLayer(layer: any, name: string | number, overlay: boolean) { - if (this.mapsService) { - layer.on('add', this.onLayerChange); - layer.on('remove', this.onLayerChange); - } - this.layers.push({ - layer, - name, - overlay, - }); - const { sortLayers, sortFunction, autoZIndex } = this.controlOption; - if (sortLayers) { - this.layers.sort((a, b) => { - return sortFunction(a.layer, b.layer, a.name, b.name); - }); - } - - if (autoZIndex && layer.setZIndex) { - this.lastZIndex++; - layer.setZIndex(this.lastZIndex); - } - - this.expandIfNotCollapsed(); - } - - private expandIfNotCollapsed() { - if (this.mapsService && !this.controlOption.collapsed) { - this.expand(); - } - return this; - } - - private onLayerChange(e: any) { - if (!this.handlingClick) { - this.update(); - } - - const obj = this.layerService.getLayer(e.target.layerId); - - // @ts-ignore - const type = obj?.overlay - ? e.type === 'add' - ? 'overlayadd' - : 'overlayremove' - : e.type === 'add' - ? 'baselayerchange' - : null; - - if (type) { - this.emit(type, obj); - } - } - - private createRadioElement(name: string, checked: boolean): ChildNode { - const radioHtml = - ''; - - const radioFragment = document.createElement('div'); - radioFragment.innerHTML = radioHtml; - - return radioFragment.firstChild as ChildNode; - } - - private addItem(obj: any) { - const label = document.createElement('label'); - const layer = this.layerService.getLayer(obj.layer.id); - const checked = layer && layer.inited && obj.layer.isVisible(); - let input: IInputItem; - if (obj.overlay) { - input = document.createElement('input') as IInputItem; - input.type = 'checkbox'; - input.className = 'l7-control-layers-selector'; - input.defaultChecked = checked; - } else { - input = this.createRadioElement('l7-base-layers', checked) as IInputItem; - } - this.layerControlInputs.push(input); - input.layerId = obj.layer.id; - input.addEventListener('click', this.onInputClick); - - const name = document.createElement('span'); - name.innerHTML = ' ' + obj.name; - - const holder = document.createElement('div'); - - label.appendChild(holder); - holder.appendChild(input); - holder.appendChild(name); - - const container = obj.overlay ? this.overlaysList : this.baseLayersList; - container.appendChild(label); - - this.checkDisabledLayers(); - return label; - } - - private onInputClick() { - const inputs = this.layerControlInputs; - let input; - let layer; - const addedLayers = []; - const removedLayers = []; - this.handlingClick = true; - for (let i = inputs.length - 1; i >= 0; i--) { - input = inputs[i]; - layer = this.layerService.getLayer(input.layerId); - if (input.checked) { - addedLayers.push(layer); - } else if (!input.checked) { - removedLayers.push(layer); - } - } - removedLayers.forEach((l: any) => { - l.hide(); - }); - addedLayers.forEach((l: any) => { - l.show(); - }); - - this.handlingClick = false; - } -} diff --git a/packages/component/src/control/layerControl.ts b/packages/component/src/control/layerControl.ts new file mode 100644 index 0000000000..1ff4feb70f --- /dev/null +++ b/packages/component/src/control/layerControl.ts @@ -0,0 +1,126 @@ +import { ILayer } from '@antv/l7-core'; +import { createL7Icon } from '../utils/icon'; +import SelectControl, { + ISelectControlOption, + ControlOptionItem, +} from './baseControl/selectControl'; + +export interface ILayerControlOption extends ISelectControlOption { + layers: ILayer[]; +} + +export { LayerControl }; + +export default class LayerControl extends SelectControl { + protected get layers() { + return this.controlOption.layers || this.layerService.getLayers() || []; + } + + public getDefault( + option?: Partial, + ): ILayerControlOption { + return { + ...super.getDefault(option), + title: '图层控制', + btnIcon: createL7Icon('l7-icon-layer'), + options: [], + }; + } + + public getLayerVisible() { + return this.layers + .filter((layer) => { + return layer.isVisible(); + }) + .map((layer) => { + return layer.name; + }); + } + + public getLayerOptions(): ControlOptionItem[] { + return this.layers.map((layer: ILayer) => { + return { + text: layer.name, + value: layer.name, + }; + }); + } + + public setOptions(option: Partial) { + const isLayerChange = this.checkUpdateOption(option, ['layers']); + if (isLayerChange) { + this.unbindLayerVisibleCallback(); + } + super.setOptions(option); + if (isLayerChange) { + this.bindLayerVisibleCallback(); + this.selectValue = this.getLayerVisible(); + this.controlOption.options = this.getLayerOptions(); + this.popper.setContent(this.getPopperContent(this.controlOption.options)); + } + } + + public onAdd(): HTMLElement { + if (!this.controlOption.options?.length) { + this.controlOption.options = this.getLayerOptions(); + } + if (!this.controlOption.defaultValue) { + this.controlOption.defaultValue = this.getLayerVisible(); + } + this.on('selectChange', this.onSelectChange); + this.layerService.on('layerChange', this.onLayerChange); + this.bindLayerVisibleCallback(); + return super.onAdd(); + } + + public bindLayerVisibleCallback = () => { + this.layers.forEach((layer) => { + layer.on('show', this.onLayerVisibleChane); + layer.on('hide', this.onLayerVisibleChane); + }); + }; + + public unbindLayerVisibleCallback = () => { + this.layers.forEach((layer) => { + layer.off('show', this.onLayerVisibleChane); + layer.off('hide', this.onLayerVisibleChane); + }); + }; + + public onRemove() { + this.off('selectChange', this.onSelectChange); + this.layerService.off('layerChange', this.onLayerChange); + this.unbindLayerVisibleCallback(); + } + + protected onLayerChange = () => { + if (this.controlOption.layers?.length) { + return; + } + this.selectValue = this.getLayerVisible(); + this.setOptions({ + options: this.getLayerOptions(), + }); + }; + + protected onLayerVisibleChane = () => { + this.setSelectValue(this.getLayerVisible()); + }; + + protected onSelectChange = () => { + this.layers.forEach((layer) => { + const needShow = this.selectValue.includes(layer.name); + const isShow = layer.isVisible(); + if (needShow && !isShow) { + layer.show(); + } + if (!needShow && isShow) { + layer.hide(); + } + }); + }; + + protected getIsMultiple(): boolean { + return true; + } +} diff --git a/packages/component/src/control/logo.ts b/packages/component/src/control/logo.ts index c708d50771..06f2d1375d 100644 --- a/packages/component/src/control/logo.ts +++ b/packages/component/src/control/logo.ts @@ -1,30 +1,63 @@ import { DOM } from '@antv/l7-utils'; -import Control, { PositionType } from './BaseControl'; +import { Control, IControlOption, PositionType } from './baseControl'; -export default class Logo extends Control { - public getDefault() { +export interface ILogoControlOption extends IControlOption { + // Logo 展示的图片 url + img: string; + // 点击 Logo 跳转的超链接,不传或传 '' | null 则纯展示 Logo,点击不跳转 + href?: string | null; +} + +export { Logo }; + +export default class Logo extends Control { + public getDefault(): ILogoControlOption { return { position: PositionType.BOTTOMLEFT, name: 'logo', + href: 'https://l7.antv.vision/', + img: + 'https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*GRb1TKp4HcMAAAAAAAAAAAAAARQnAQ', }; } + public onAdd() { - const className = 'l7-control-logo'; - const container = DOM.create('div', className); - const anchor: HTMLLinkElement = DOM.create( - 'a', - 'l7-ctrl-logo', - ) as HTMLLinkElement; - anchor.target = '_blank'; - anchor.rel = 'noopener nofollow'; - anchor.href = 'https://antv.alipay.com/l7'; - anchor.setAttribute('aria-label', 'AntV logo'); - anchor.setAttribute('rel', 'noopener nofollow'); - container.appendChild(anchor); + const container = DOM.create('div', 'l7-control-logo'); + this.setLogoContent(container); return container; } public onRemove() { return null; } + + public setOptions(option: Partial) { + super.setOptions(option); + if (this.checkUpdateOption(option, ['img', 'href'])) { + DOM.clearChildren(this.container); + this.setLogoContent(this.container); + } + } + + protected setLogoContent(container: HTMLElement) { + const { href, img } = this.controlOption; + const imgDOM = DOM.create('img') as HTMLElement; + imgDOM.setAttribute('src', img); + imgDOM.setAttribute('aria-label', 'AntV logo'); + DOM.setUnDraggable(imgDOM); + if (href) { + const anchorDOM = DOM.create( + 'a', + 'l7-control-logo-link', + ) as HTMLLinkElement; + anchorDOM.target = '_blank'; + anchorDOM.href = href; + anchorDOM.rel = 'noopener nofollow'; + anchorDOM.setAttribute('rel', 'noopener nofollow'); + anchorDOM.appendChild(imgDOM); + container.appendChild(anchorDOM); + } else { + container.appendChild(imgDOM); + } + } } diff --git a/packages/component/src/control/mapTheme.ts b/packages/component/src/control/mapTheme.ts new file mode 100644 index 0000000000..d144aed148 --- /dev/null +++ b/packages/component/src/control/mapTheme.ts @@ -0,0 +1,78 @@ +import { GaodeMapStyleConfig, MapboxMapStyleConfig } from '../constants'; +import { createL7Icon } from '../utils/icon'; +import SelectControl, { + ControlOptionItem, + ISelectControlOption, +} from './baseControl/selectControl'; + +export { MapTheme }; + +export default class MapTheme extends SelectControl { + public getDefault( + option?: Partial, + ): ISelectControlOption { + return { + ...super.getDefault(option), + title: '地图样式', + btnIcon: createL7Icon('l7-icon-color'), + options: [], + }; + } + + public getStyleOptions(): ControlOptionItem[] { + const mapStyleConfig = + this.mapsService.getType() === 'mapbox' + ? MapboxMapStyleConfig + : GaodeMapStyleConfig; + return Object.entries(this.mapsService.getMapStyleConfig()) + .filter(([key, value]) => typeof value === 'string' && key !== 'blank') + .map(([key, value]) => { + // @ts-ignore + const { text, img } = mapStyleConfig[key] ?? {}; + return { + text: text ?? key, + value, + img, + key, + }; + }); + } + + public getMapStyle() { + return this.mapsService.getMapStyle(); + } + + public onAdd(): HTMLElement { + if (!this.controlOption.options?.length) { + this.controlOption.options = this.getStyleOptions(); + } + if (this.controlOption.defaultValue) { + const defaultValue = this.controlOption.defaultValue as string; + this.controlOption.defaultValue = + this.controlOption.options.find((item) => item.key === defaultValue) + ?.value ?? defaultValue; + } else { + const defaultStyle = this.getMapStyle(); + if (defaultStyle) { + this.controlOption.defaultValue = defaultStyle; + } else { + // @ts-ignore + this.mapsService.map.once('styledata', () => { + const mapboxStyle = this.mapsService.getMapStyle(); + this.controlOption.defaultValue = mapboxStyle; + this.setSelectValue(mapboxStyle, false); + }); + } + } + this.on('selectChange', this.onMapThemeChange); + return super.onAdd(); + } + + protected onMapThemeChange = () => { + this.mapsService.setMapStyle(this.selectValue[0]); + }; + + protected getIsMultiple(): boolean { + return false; + } +} diff --git a/packages/component/src/control/mouseLocation.ts b/packages/component/src/control/mouseLocation.ts new file mode 100644 index 0000000000..9574056607 --- /dev/null +++ b/packages/component/src/control/mouseLocation.ts @@ -0,0 +1,60 @@ +import { ILngLat, Position, PositionType } from '@antv/l7-core'; +import { DOM } from '@antv/l7-utils'; +import Control, { IControlOption } from './baseControl/control'; + +export interface IMouseLocationControlOption extends IControlOption { + transform: (position: Position) => Position; +} + +export { MouseLocation }; + +export default class MouseLocation extends Control< + IMouseLocationControlOption +> { + protected location: Position = [0, 0]; + + public getLocation() { + return this.location; + } + + public getDefault( + option?: Partial, + ): IMouseLocationControlOption { + return { + ...super.getDefault(option), + position: PositionType.BOTTOMLEFT, + transform: ([lng, lat]) => { + return [+(+lng).toFixed(6), +(+lat).toFixed(6)]; + }, + }; + } + + public onAdd(): HTMLElement { + const container = DOM.create('div', 'l7-control-mouse-location'); + container.innerHTML = ' '; + this.mapsService.on('mousemove', this.onMouseMove); + return container; + } + + public onRemove(): void { + this.mapsService.off('mousemove', this.onMouseMove); + } + protected onMouseMove = (e: any) => { + let position: Position = this.location; + const lngLat: ILngLat | undefined = e.lngLat || e.lnglat; + const { transform } = this.controlOption; + if (lngLat) { + position = [lngLat.lng, lngLat.lat]; + } + this.location = position; + if (transform) { + position = transform(position); + } + this.insertLocation2HTML(position); + this.emit('locationChange', position); + }; + + protected insertLocation2HTML(position: Position) { + this.container.innerText = position.join(', '); + } +} diff --git a/packages/component/src/control/scale.ts b/packages/component/src/control/scale.ts index 8dd313eded..aa81ab2f17 100644 --- a/packages/component/src/control/scale.ts +++ b/packages/component/src/control/scale.ts @@ -1,39 +1,43 @@ -import { bindAll, DOM, lnglatDistance } from '@antv/l7-utils'; -import { IScaleControlOption } from '../interface'; -import Control, { PositionType } from './BaseControl'; +import { DOM, lnglatDistance } from '@antv/l7-utils'; +import { Control, IControlOption, PositionType } from './baseControl'; -export default class Scale extends Control { +export interface IScaleControlOption extends IControlOption { + lockWidth: boolean; + maxWidth: number; + metric: boolean; + updateWhenIdle: boolean; + imperial: boolean; +} + +export { Scale }; + +export default class Scale extends Control { private mScale: HTMLElement; private iScale: HTMLElement; - constructor(cfg?: Partial) { - super(cfg); - bindAll(['update'], this); - } - public getDefault() { + public getDefault(option: Partial) { return { + ...super.getDefault(option), + name: 'scale', position: PositionType.BOTTOMLEFT, maxWidth: 100, metric: true, updateWhenIdle: false, imperial: false, - name: 'scale', + lockWidth: true, }; } public onAdd() { const className = 'l7-control-scale'; const container = DOM.create('div', className); - this.addScales(className + '-line', container); + this.resetScaleLines(container); const { updateWhenIdle } = this.controlOption; - // TODO: 高德地图和MapBox地图事件不一致问题 - // 高德zoomchange this.mapsService.on(updateWhenIdle ? 'moveend' : 'mapmove', this.update); this.mapsService.on(updateWhenIdle ? 'zoomend' : 'zoomchange', this.update); - this.update(); - return container; } + public onRemove() { const { updateWhenIdle } = this.controlOption; this.mapsService.off( @@ -42,7 +46,23 @@ export default class Scale extends Control { ); this.mapsService.off(updateWhenIdle ? 'moveend' : 'mapmove', this.update); } - public update() { + + public setOptions(newOption: Partial) { + super.setOptions(newOption); + if ( + this.checkUpdateOption(newOption, [ + 'lockWidth', + 'maxWidth', + 'metric', + 'updateWhenIdle', + 'imperial', + ]) + ) { + this.resetScaleLines(this.container); + } + } + + public update = () => { const mapsService = this.mapsService; const { maxWidth } = this.controlOption; const y = mapsService.getSize()[1] / 2; @@ -51,7 +71,8 @@ export default class Scale extends Control { const p2 = mapsService.containerToLngLat([maxWidth, y]); const maxMeters = lnglatDistance([p1.lng, p1.lat], [p2.lng, p2.lat]); this.updateScales(maxMeters); - } + }; + public updateScales(maxMeters: number) { const { metric, imperial } = this.controlOption; if (metric && maxMeters) { @@ -61,11 +82,42 @@ export default class Scale extends Control { this.updateImperial(maxMeters); } } + + private resetScaleLines(container: HTMLElement) { + DOM.clearChildren(container); + const { metric, imperial, maxWidth, lockWidth } = this.controlOption; + if (lockWidth) { + DOM.addStyle(container, `width: ${maxWidth}px`); + } + if (metric) { + this.mScale = DOM.create('div', 'l7-control-scale-line', container); + } + if (imperial) { + this.iScale = DOM.create('div', 'l7-control-scale-line', container); + } + this.update(); + } + + private updateScale(scale: HTMLElement, text: string, ratio: number) { + const { maxWidth } = this.controlOption; + scale.style.width = Math.round(maxWidth * ratio) + 'px'; + scale.innerHTML = text; + } + private getRoundNum(num: number) { + const pow10 = Math.pow(10, (Math.floor(num) + '').length - 1); + let d = num / pow10; + + d = d >= 10 ? 10 : d >= 5 ? 5 : d >= 3 ? 3 : d >= 2 ? 2 : 1; + + return pow10 * d; + } + private updateMetric(maxMeters: number) { const meters = this.getRoundNum(maxMeters); const label = meters < 1000 ? meters + ' m' : meters / 1000 + ' km'; this.updateScale(this.mScale, label, meters / maxMeters); } + private updateImperial(maxMeters: number) { const maxFeet = maxMeters * 3.2808399; let maxMiles: number; @@ -81,26 +133,4 @@ export default class Scale extends Control { this.updateScale(this.iScale, feet + ' ft', feet / maxFeet); } } - private updateScale(scale: HTMLElement, text: string, ratio: number) { - const { maxWidth } = this.controlOption; - scale.style.width = Math.round(maxWidth * ratio) + 'px'; - scale.innerHTML = text; - } - private getRoundNum(num: number) { - const pow10 = Math.pow(10, (Math.floor(num) + '').length - 1); - let d = num / pow10; - - d = d >= 10 ? 10 : d >= 5 ? 5 : d >= 3 ? 3 : d >= 2 ? 2 : 1; - - return pow10 * d; - } - private addScales(className: string, container: HTMLElement) { - const { metric, imperial } = this.controlOption; - if (metric) { - this.mScale = DOM.create('div', className, container); - } - if (imperial) { - this.iScale = DOM.create('div', className, container); - } - } } diff --git a/packages/component/src/control/zoom.ts b/packages/component/src/control/zoom.ts index 9dd74120dd..499e664e2b 100644 --- a/packages/component/src/control/zoom.ts +++ b/packages/component/src/control/zoom.ts @@ -1,48 +1,53 @@ -import { bindAll, DOM } from '@antv/l7-utils'; -import { IZoomControlOption } from '../interface'; -import Control, { PositionType } from './BaseControl'; +import { PositionType } from '@antv/l7-core'; +import { DOM } from '@antv/l7-utils'; +import { ELType } from '@antv/l7-utils/src/dom'; +import { createL7Icon } from '../utils/icon'; +import { Control, IControlOption } from './baseControl'; -export default class Zoom extends Control { +export interface IZoomControlOption extends IControlOption { + zoomInText: ELType | string; + zoomInTitle: string; + zoomOutText: ELType | string; + zoomOutTitle: string; +} + +export { Zoom }; + +export default class Zoom extends Control { private disabled: boolean; private zoomInButton: HTMLElement; private zoomOutButton: HTMLElement; - constructor(cfg?: Partial) { - super(cfg); - bindAll(['updateDisabled', 'zoomIn', 'zoomOut'], this); - } - public getDefault() { + public getDefault(option: Partial) { return { - position: PositionType.TOPLEFT, - zoomInText: '+', - zoomInTitle: 'Zoom in', - zoomOutText: '−', - zoomOutTitle: 'Zoom out', + ...super.getDefault(option), + position: PositionType.BOTTOMRIGHT, name: 'zoom', + zoomInText: createL7Icon('l7-icon-enlarge'), + zoomInTitle: 'Zoom in', + zoomOutText: createL7Icon('l7-icon-narrow'), + zoomOutTitle: 'Zoom out', }; } + public setOptions(newOptions: Partial) { + super.setOptions(newOptions); + if ( + this.checkUpdateOption(newOptions, [ + 'zoomInText', + 'zoomInTitle', + 'zoomOutText', + 'zoomOutTitle', + ]) + ) { + this.resetButtonGroup(this.container); + } + } public onAdd(): HTMLElement { - const zoomName = 'l7-control-zoom'; - const container = DOM.create('div', zoomName + ' l7-bar'); - - this.zoomInButton = this.createButton( - this.controlOption.zoomInText, - this.controlOption.zoomInTitle, - zoomName + '-in', - container, - this.zoomIn, - ); - this.zoomOutButton = this.createButton( - this.controlOption.zoomOutText, - this.controlOption.zoomOutTitle, - zoomName + '-out', - container, - this.zoomOut, - ); + const container = DOM.create('div', 'l7-control-zoom'); + this.resetButtonGroup(container); this.mapsService.on('zoomend', this.updateDisabled); this.mapsService.on('zoomchange', this.updateDisabled); - this.updateDisabled(); return container; } @@ -63,46 +68,70 @@ export default class Zoom extends Control { return this; } - private zoomIn() { + public zoomIn = () => { if ( !this.disabled && this.mapsService.getZoom() < this.mapsService.getMaxZoom() ) { this.mapsService.zoomIn(); } - } - private zoomOut() { + }; + + public zoomOut = () => { if ( !this.disabled && this.mapsService.getZoom() > this.mapsService.getMinZoom() ) { this.mapsService.zoomOut(); } + }; + + private resetButtonGroup(container: HTMLElement) { + DOM.clearChildren(container); + this.zoomInButton = this.createButton( + this.controlOption.zoomInText, + this.controlOption.zoomInTitle, + 'l7-button-control', + container, + this.zoomIn, + ); + this.zoomOutButton = this.createButton( + this.controlOption.zoomOutText, + this.controlOption.zoomOutTitle, + 'l7-button-control', + container, + this.zoomOut, + ); + this.updateDisabled(); } + private createButton( - html: string, + html: ELType | string, tile: string, className: string, container: HTMLElement, fn: (...arg: any[]) => any, ) { - const link = DOM.create('a', className, container) as HTMLLinkElement; - link.innerHTML = html; + const link = DOM.create('button', className, container) as HTMLLinkElement; + if (typeof html === 'string') { + link.innerHTML = html; + } else { + link.append(html); + } link.title = tile; - link.href = 'javascript:void(0)'; link.addEventListener('click', fn); return link; } - private updateDisabled() { + + private updateDisabled = () => { const mapsService = this.mapsService; - const className = 'l7-disabled'; - DOM.removeClass(this.zoomInButton, className); - DOM.removeClass(this.zoomOutButton, className); + this.zoomInButton.removeAttribute('disabled'); + this.zoomOutButton.removeAttribute('disabled'); if (this.disabled || mapsService.getZoom() <= mapsService.getMinZoom()) { - DOM.addClass(this.zoomOutButton, className); + this.zoomOutButton.setAttribute('disabled', 'true'); } if (this.disabled || mapsService.getZoom() >= mapsService.getMaxZoom()) { - DOM.addClass(this.zoomInButton, className); + this.zoomInButton.setAttribute('disabled', 'true'); } - } + }; } diff --git a/packages/component/src/css/button.less b/packages/component/src/css/button.less new file mode 100644 index 0000000000..c968e4aca1 --- /dev/null +++ b/packages/component/src/css/button.less @@ -0,0 +1,70 @@ +@import 'variables.less'; + +.l7-button-control { + min-width: @l7-btn-control-size; + height: @l7-btn-control-size; + background-color: @l7-control-bg-color; + border-width: 0; + border-radius: @l7-btn-control-border-radius; + outline: 0; + cursor: pointer; + transition: all 0.2s; + display: flex; + justify-content: center; + align-items: center; + padding: 0 ((@l7-btn-control-size - @l7-btn-icon-size) / 2); + box-shadow: @l7-control-shadow; + line-height: 16px; + + .l7-iconfont { + fill: @l7-control-font-color; + color: @l7-control-font-color; + width: @l7-btn-icon-size; + height: @l7-btn-icon-size; + } + + &.l7-button-control--row { + padding: 0 16px 0 13px; + + * + .l7-button-control__text { + margin-left: 8px; + } + } + + &.l7-button-control--column { + height: @l7-btn-column-height; + flex-direction: column; + .l7-iconfont { + margin-top: 3px; + } + .l7-button-control__text { + margin-top: 3px; + font-size: 10px; + transform: scale(0.83333); + } + } + + &:not(:disabled) { + &:hover { + background-color: @l7-btn-control-bg-hover-color; + } + &:active { + background-color: @l7-btn-control-bg-active-color; + } + } + &:disabled { + background-color: @l7-btn-control-disabled-bg-color; + color: @l7-btn-control-disabled-font-color; + cursor: not-allowed; + .l7-iconfont { + fill: @l7-btn-control-disabled-font-color; + color: @l7-btn-control-disabled-font-color; + } + &:hover { + background-color: @l7-btn-control-disabled-bg-color; + } + &:active { + background-color: @l7-btn-control-disabled-bg-color; + } + } +} diff --git a/packages/component/src/css/control.less b/packages/component/src/css/control.less new file mode 100644 index 0000000000..7010e1a194 --- /dev/null +++ b/packages/component/src/css/control.less @@ -0,0 +1,71 @@ +@import 'variables.less'; + +.l7-control-container { + font: 12px/1.5 'Helvetica Neue', Arial, Helvetica, sans-serif; + + .l7-control { + position: relative; + z-index: 800; + float: left; + clear: both; + color: @l7-control-font-color; + font-size: @l7-control-font-size; + pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ + pointer-events: auto; + + &.l7-control--hide { + display: none; + } + } + + each(@position-list,{ + .l7-@{value} { + @{value}: 0; + display: flex; + position: absolute; + z-index: 1000; + pointer-events: none; + .l7-control:not(.l7-control--hide) { + margin-@{value}: @l7-control-space; + } + } + }); + + .l7-center { + position: absolute; + display: flex; + justify-content: center; + &.l7-top, + &.l7-bottom { + width: 100%; + } + &.l7-left, + &.l7-right { + height: 100%; + } + .l7-control { + margin-right: @l7-control-space; + margin-bottom: @l7-control-space; + } + } + + .l7-row { + flex-direction: row; + &.l7-top { + align-items: flex-start; + } + &.l7-bottom { + align-items: flex-end; + } + } + + .l7-column { + flex-direction: column; + &.l7-left { + align-items: flex-start; + } + &.l7-right { + align-items: flex-end; + } + } +} diff --git a/packages/component/src/css/index.css b/packages/component/src/css/index.css new file mode 100644 index 0000000000..f5715f75e2 --- /dev/null +++ b/packages/component/src/css/index.css @@ -0,0 +1,567 @@ +.l7-marker-container { + position: absolute; + width: 100%; + height: 100%; + overflow: hidden; +} +.l7-marker { + position: absolute !important; + top: 0; + left: 0; + z-index: 5; + cursor: pointer; +} +.l7-marker-cluster { + width: 40px; + height: 40px; + background-color: rgba(181, 226, 140, 0.6); + background-clip: padding-box; + border-radius: 20px; +} +.l7-marker-cluster div { + width: 30px; + height: 30px; + margin-top: 5px; + margin-left: 5px; + font: 12px 'Helvetica Neue', Arial, Helvetica, sans-serif; + text-align: center; + background-color: rgba(110, 204, 57, 0.6); + border-radius: 15px; +} +.l7-marker-cluster span { + line-height: 30px; +} +.l7-touch .l7-control-attribution, +.l7-touch .l7-control-layers, +.l7-touch .l7-bar { + box-shadow: none; +} +.l7-touch .l7-control-layers, +.l7-touch .l7-bar { + background-clip: padding-box; + border: 2px solid rgba(0, 0, 0, 0.2); +} +.mapboxgl-ctrl-logo, +.amap-logo { + display: none !important; +} +.l7-select-box { + border: 3px dashed gray; + border-radius: 2px; + position: absolute; + z-index: 1000; + box-sizing: border-box; +} +.l7-control-container { + font: 12px/1.5 'Helvetica Neue', Arial, Helvetica, sans-serif; +} +.l7-control-container .l7-control { + position: relative; + z-index: 800; + float: left; + clear: both; + color: #595959; + font-size: 12px; + pointer-events: visiblePainted; + /* IE 9-10 doesn't have auto */ + pointer-events: auto; +} +.l7-control-container .l7-control.l7-control--hide { + display: none; +} +.l7-control-container .l7-top { + top: 0; + display: flex; + position: absolute; + z-index: 1000; + pointer-events: none; +} +.l7-control-container .l7-top .l7-control:not(.l7-control--hide) { + margin-top: 8px; +} +.l7-control-container .l7-right { + right: 0; + display: flex; + position: absolute; + z-index: 1000; + pointer-events: none; +} +.l7-control-container .l7-right .l7-control:not(.l7-control--hide) { + margin-right: 8px; +} +.l7-control-container .l7-bottom { + bottom: 0; + display: flex; + position: absolute; + z-index: 1000; + pointer-events: none; +} +.l7-control-container .l7-bottom .l7-control:not(.l7-control--hide) { + margin-bottom: 8px; +} +.l7-control-container .l7-left { + left: 0; + display: flex; + position: absolute; + z-index: 1000; + pointer-events: none; +} +.l7-control-container .l7-left .l7-control:not(.l7-control--hide) { + margin-left: 8px; +} +.l7-control-container .l7-center { + position: absolute; + display: flex; + justify-content: center; +} +.l7-control-container .l7-center.l7-top, +.l7-control-container .l7-center.l7-bottom { + width: 100%; +} +.l7-control-container .l7-center.l7-left, +.l7-control-container .l7-center.l7-right { + height: 100%; +} +.l7-control-container .l7-center .l7-control { + margin-right: 8px; + margin-bottom: 8px; +} +.l7-control-container .l7-row { + flex-direction: row; +} +.l7-control-container .l7-row.l7-top { + align-items: flex-start; +} +.l7-control-container .l7-row.l7-bottom { + align-items: flex-end; +} +.l7-control-container .l7-column { + flex-direction: column; +} +.l7-control-container .l7-column.l7-left { + align-items: flex-start; +} +.l7-control-container .l7-column.l7-right { + align-items: flex-end; +} +.l7-button-control { + min-width: 28px; + height: 28px; + background-color: #fff; + border-width: 0; + border-radius: 2px; + outline: 0; + cursor: pointer; + transition: all 0.2s; + display: flex; + justify-content: center; + align-items: center; + padding: 0 6px; + box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.15); + line-height: 16px; +} +.l7-button-control .l7-iconfont { + fill: #595959; + color: #595959; + width: 16px; + height: 16px; +} +.l7-button-control.l7-button-control--row { + padding: 0 16px 0 13px; +} +.l7-button-control.l7-button-control--row * + .l7-button-control__text { + margin-left: 8px; +} +.l7-button-control.l7-button-control--column { + height: 44px; + flex-direction: column; +} +.l7-button-control.l7-button-control--column .l7-iconfont { + margin-top: 3px; +} +.l7-button-control.l7-button-control--column .l7-button-control__text { + margin-top: 3px; + font-size: 10px; + transform: scale(0.83333); +} +.l7-button-control:not(:disabled):hover { + background-color: #f3f3f3; +} +.l7-button-control:not(:disabled):active { + background-color: #f3f3f3; +} +.l7-button-control:disabled { + background-color: #fafafa; + color: #bdbdbd; + cursor: not-allowed; +} +.l7-button-control:disabled .l7-iconfont { + fill: #bdbdbd; + color: #bdbdbd; +} +.l7-button-control:disabled:hover { + background-color: #fafafa; +} +.l7-button-control:disabled:active { + background-color: #fafafa; +} +.l7-popper { + position: absolute; + display: flex; + justify-content: center; + align-items: center; + z-index: 5; + color: #595959; +} +.l7-popper.l7-popper-hide { + display: none; +} +.l7-popper .l7-popper-content { + min-height: 28px; + background: #fff; + border-radius: 2px; + box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.15); +} +.l7-popper .l7-popper-arrow { + width: 0; + height: 0; + border-width: 4px; + border-style: solid; + border-top-color: transparent; + border-bottom-color: transparent; + border-left-color: transparent; + border-right-color: transparent; + box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.15); +} +.l7-popper.l7-popper-left { + flex-direction: row; +} +.l7-popper.l7-popper-left .l7-popper-arrow { + border-left-color: #fff; + margin: 10px 0; +} +.l7-popper.l7-popper-right { + flex-direction: row-reverse; +} +.l7-popper.l7-popper-right .l7-popper-arrow { + border-right-color: #fff; + margin: 10px 0; +} +.l7-popper.l7-popper-top { + flex-direction: column; +} +.l7-popper.l7-popper-top .l7-popper-arrow { + border-top-color: #fff; + margin: 0 10px; +} +.l7-popper.l7-popper-bottom { + flex-direction: column-reverse; +} +.l7-popper.l7-popper-bottom .l7-popper-arrow { + border-bottom-color: #fff; + margin: 0 10px; +} +.l7-popper.l7-popper-start { + align-items: flex-start; +} +.l7-popper.l7-popper-end { + align-items: flex-end; +} +.l7-select-control--normal { + padding: 4px 0; +} +.l7-select-control--normal .l7-select-control-item { + height: 24px; + line-height: 24px; + display: flex; + align-items: center; + padding: 0 16px; + font-size: 12px; +} +.l7-select-control--normal .l7-select-control-item > * + * { + margin-left: 6px; +} +.l7-select-control--normal .l7-select-control-item input[type='checkbox'] { + height: 14px; + width: 14px; +} +.l7-select-control--normal .l7-select-control-item:hover { + background-color: #f3f3f3; +} +.l7-select-control--image { + padding: 12px 12px 0 12px; + width: 474px; + height: 320px; + overflow: auto; + display: flex; + flex-wrap: wrap; + box-sizing: border-box; + align-items: flex-start; +} +.l7-select-control--image .l7-select-control-item { + margin-right: 12px; + border-radius: 2px; + overflow: hidden; + border: 1px solid #fff; + box-sizing: content-box; + width: calc((100% - 36px) / 3); + display: flex; + flex-direction: column; + justify-content: center; + margin-bottom: 12px; + position: relative; + font-size: 12px; +} +.l7-select-control--image .l7-select-control-item img { + width: 142px; + height: 80px; +} +.l7-select-control--image .l7-select-control-item input[type='checkbox'] { + position: absolute; + right: 0; + top: 0; +} +.l7-select-control--image .l7-select-control-item .l7-select-control-item-row { + display: flex; + justify-content: center; + align-items: center; + line-height: 26px; +} +.l7-select-control--image .l7-select-control-item .l7-select-control-item-row > * + * { + margin-left: 8px; +} +.l7-select-control--image .l7-select-control-item.l7-select-control-item-active { + border-color: #0370fe; +} +.l7-select-control--image .l7-select-control-item:nth-child(3n) { + margin-right: 0; +} +.l7-select-control-item { + cursor: pointer; +} +.l7-select-control-item input[type='checkbox'] { + margin: 0; + cursor: pointer; +} +.l7-select-control--multiple .l7-select-control-item:hover { + background-color: transparent; +} +.l7-control-logo { + width: 89px; + height: 16px; + user-select: none; +} +.l7-control-logo img { + height: 100%; + width: 100%; +} +.l7-control-logo .l7-control-logo-link { + display: block; + cursor: pointer; +} +.l7-control-logo .l7-control-logo-link img { + cursor: pointer; +} +.l7-control-mouse-location { + background-color: #fff; + border-radius: 2px; + box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.15); + padding: 2px 4px; + min-width: 130px; +} +.l7-control-zoom { + box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.15); + border-radius: 2px; + overflow: hidden; +} +.l7-control-zoom .l7-button-control { + box-shadow: 0 0 0; + border-radius: 0; + font-size: 16px; +} +.l7-control-zoom .l7-button-control .l7-iconfont { + width: 14px; + height: 14px; +} +.l7-control-zoom .l7-button-control:first-child { + border-bottom: 1px solid #f0f0f0; +} +.l7-control-scale { + display: flex; + flex-direction: column; +} +.l7-control-scale .l7-control-scale-line { + box-sizing: border-box; + padding: 2px 5px 1px; + overflow: hidden; + color: #595959; + font-size: 10px; + line-height: 1.1; + white-space: nowrap; + background: #fff; + border: 2px solid #000; + border-top: 0; + transition: width 0.1s; +} +.l7-control-scale .l7-control-scale-line + .l7-control-scale .l7-control-scale-line { + margin-top: -2px; + border-top: 2px solid #777; + border-bottom: none; +} +.l7-right .l7-control-scale { + display: flex; + align-items: flex-end; +} +.l7-right .l7-control-scale .l7-control-scale-line { + text-align: right; +} +.l7-popup { + position: absolute; + top: 0; + left: 0; + z-index: 5; + display: -webkit-flex; + display: flex; + pointer-events: none; + will-change: transform; +} +.l7-popup.l7-popup-hide { + display: none; +} +.l7-popup .l7-popup-content { + position: relative; + padding: 16px; + background: #fff; + border-radius: 3px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + pointer-events: auto; + font-size: 14px; +} +.l7-popup .l7-popup-content .l7-popup-content__title { + font-weight: bold; + margin-bottom: 8px; +} +.l7-popup .l7-popup-content .l7-popup-close-button { + position: absolute; + top: 0; + right: 0; + width: 18px; + height: 18px; + line-height: 18px; + text-align: center; + padding: 0; + background-color: transparent; + border: 0; + border-radius: 0 3px 0 0; + cursor: pointer; + font-size: 14px; +} +.l7-popup .l7-popup-tip { + z-index: 1; + width: 0; + height: 0; + border: 10px solid transparent; +} +.l7-popup.l7-popup-anchor-bottom, +.l7-popup.l7-popup-anchor-bottom-left, +.l7-popup.l7-popup-anchor-bottom-right { + -webkit-flex-direction: column-reverse; + flex-direction: column-reverse; +} +.l7-popup.l7-popup-anchor-top, +.l7-popup.l7-popup-anchor-top-left, +.l7-popup.l7-popup-anchor-top-right { + -webkit-flex-direction: column; + flex-direction: column; +} +.l7-popup.l7-popup-anchor-left { + -webkit-flex-direction: row; + flex-direction: row; +} +.l7-popup.l7-popup-anchor-right { + -webkit-flex-direction: row-reverse; + flex-direction: row-reverse; +} +.l7-popup-anchor-top .l7-popup-tip { + -webkit-align-self: center; + align-self: center; + border-top: none; + border-bottom-color: #fff; +} +.l7-popup-anchor-top-left .l7-popup-tip { + -webkit-align-self: flex-start; + align-self: flex-start; + border-top: none; + border-bottom-color: #fff; + border-left: none; +} +.l7-popup-anchor-top-right .l7-popup-tip { + -webkit-align-self: flex-end; + align-self: flex-end; + border-top: none; + border-right: none; + border-bottom-color: #fff; +} +.l7-popup-anchor-bottom .l7-popup-tip { + -webkit-align-self: center; + align-self: center; + border-top-color: #fff; + border-bottom: none; +} +.l7-popup-anchor-bottom-left .l7-popup-tip { + -webkit-align-self: flex-start; + align-self: flex-start; + border-top-color: #fff; + border-bottom: none; + border-left: none; +} +.l7-popup-anchor-bottom-right .l7-popup-tip { + -webkit-align-self: flex-end; + align-self: flex-end; + border-top-color: #fff; + border-right: none; + border-bottom: none; +} +.l7-popup-anchor-left .l7-popup-tip { + -webkit-align-self: center; + align-self: center; + border-right-color: #fff; + border-left: none; +} +.l7-popup-anchor-right .l7-popup-tip { + -webkit-align-self: center; + align-self: center; + border-right: none; + border-left-color: #fff; +} +.l7-popup-anchor-top-left .l7-popup-content { + border-top-left-radius: 0; +} +.l7-popup-anchor-top-right .l7-popup-content { + border-top-right-radius: 0; +} +.l7-popup-anchor-bottom-left .l7-popup-content { + border-bottom-left-radius: 0; +} +.l7-popup-anchor-bottom-right .l7-popup-content { + border-bottom-right-radius: 0; +} +.l7-popup-track-pointer { + display: none; +} +.l7-popup-track-pointer * { + user-select: none; + pointer-events: none; +} +.l7-map:hover .l7-popup-track-pointer { + display: flex; +} +.l7-map:active .l7-popup-track-pointer { + display: none; +} +.l7-layer-popup__row { + font-size: 12px; +} +.l7-layer-popup__row + .l7-layer-popup__row { + margin-top: 4px; +} diff --git a/packages/component/src/css/index.less b/packages/component/src/css/index.less new file mode 100644 index 0000000000..16d43b8ba2 --- /dev/null +++ b/packages/component/src/css/index.less @@ -0,0 +1,12 @@ +@import 'variables'; +@import 'l7'; +@import 'control'; +@import 'button'; +@import 'popper'; +@import 'select'; +@import 'logo'; +@import 'mouseLocation'; +@import 'zoom'; +@import 'scale'; +@import 'popup'; +@import 'layerPopup'; diff --git a/packages/component/src/css/l7.css b/packages/component/src/css/l7.css deleted file mode 100644 index 79ef9405b2..0000000000 --- a/packages/component/src/css/l7.css +++ /dev/null @@ -1,502 +0,0 @@ -.l7-marker-container { - width: 100%; - height: 100%; - overflow: hidden; - position: absolute; -} - -.l7-marker { - position: absolute !important; - top: 0; - left: 0; - z-index: 5; - cursor: pointer; -} - -.l7-marker-cluster { - background-clip: padding-box; - border-radius: 20px; - background-color: rgba(181, 226, 140, 0.6); - width: 40px; - height: 40px; -} -.l7-marker-cluster div { - width: 30px; - height: 30px; - margin-left: 5px; - margin-top: 5px; - text-align: center; - border-radius: 15px; - font: 12px 'Helvetica Neue', Arial, Helvetica, sans-serif; - background-color: rgba(110, 204, 57, 0.6); -} -.l7-marker-cluster span { - line-height: 30px; -} - -.l7-popup-anchor-bottom, -.l7-popup-anchor-bottom-left, -.l7-popup-anchor-bottom-right { - -webkit-flex-direction: column-reverse; - flex-direction: column-reverse; -} - -.l7-popup-close-button { - position: absolute; - right: 0; - top: 0; - border: 0; - border-radius: 0 3px 0 0; - cursor: pointer; - background-color: transparent; -} - -.l7-popup-close-button:hover { - background-color: rgba(0, 0, 0, 0.05); -} - -.l7-popup-content { - position: relative; - background: #fff; - border-radius: 3px; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); - padding: 10px 10px 15px; - pointer-events: auto; -} - -/* layers control */ - -.l7-control-layers { - box-shadow: 0 1px 5px rgba(0, 0, 0, 0.4); - background: #fff; - border-radius: 5px; -} -.l7-popup-anchor-top, -.l7-popup-anchor-top-left, -.l7-popup-anchor-top-right { - -webkit-flex-direction: column; - flex-direction: column; -} - -.l7-popup-anchor-left { - -webkit-flex-direction: row; - flex-direction: row; -} - -.l7-popup-anchor-right { - -webkit-flex-direction: row-reverse; - flex-direction: row-reverse; -} -.l7-popup { - position: absolute; - top: 0; - left: 0; - display: -webkit-flex; - display: flex; - will-change: transform; - pointer-events: none; - z-index: 5; -} -.l7-popup-tip { - width: 0; - height: 0; - border: 10px solid transparent; - z-index: 1; -} -.l7-popup-anchor-top .l7-popup-tip { - -webkit-align-self: center; - align-self: center; - border-top: none; - border-bottom-color: #fff; -} - -.l7-popup-anchor-top-left .l7-popup-tip { - -webkit-align-self: flex-start; - align-self: flex-start; - border-top: none; - border-left: none; - border-bottom-color: #fff; -} - -.l7-popup-anchor-top-right .l7-popup-tip { - -webkit-align-self: flex-end; - align-self: flex-end; - border-top: none; - border-right: none; - border-bottom-color: #fff; -} - -.l7-popup-anchor-bottom .l7-popup-tip { - -webkit-align-self: center; - align-self: center; - border-bottom: none; - border-top-color: #fff; -} - -.l7-popup-anchor-bottom-left .l7-popup-tip { - -webkit-align-self: flex-start; - align-self: flex-start; - border-bottom: none; - border-left: none; - border-top-color: #fff; -} - -.l7-popup-anchor-bottom-right .l7-popup-tip { - -webkit-align-self: flex-end; - align-self: flex-end; - border-bottom: none; - border-right: none; - border-top-color: #fff; -} - -.l7-popup-anchor-left .l7-popup-tip { - -webkit-align-self: center; - align-self: center; - border-left: none; - border-right-color: #fff; -} - -.l7-popup-anchor-right .l7-popup-tip { - -webkit-align-self: center; - align-self: center; - border-right: none; - border-left-color: #fff; -} - -.l7-popup-close-button { - position: absolute; - right: 0; - top: 0; - border: 0; - padding: 0; - font-size: 25px; - line-height: 20px; - border-radius: 0 3px 0 0; - cursor: pointer; - background-color: transparent; -} - -.l7-popup-close-button:hover { - background-color: rgba(0, 0, 0, 0.05); -} - -.l7-popup-content { - position: relative; - background: #fff; - border-radius: 3px; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); - padding: 10px 10px 15px; - pointer-events: auto; -} - -.l7-popup-anchor-top-left .l7-popup-content { - border-top-left-radius: 0; -} - -.l7-popup-anchor-top-right .l7-popup-content { - border-top-right-radius: 0; -} - -.l7-popup-anchor-bottom-left .l7-popup-content { - border-bottom-left-radius: 0; -} - -.l7-popup-anchor-bottom-right .l7-popup-content { - border-bottom-right-radius: 0; -} - -.l7-popup-track-pointer { - display: none; -} - -.l7-popup-track-pointer * { - pointer-events: none; - user-select: none; -} - -.l7-map:hover .l7-popup-track-pointer { - display: flex; -} - -.l7-map:active .l7-popup-track-pointer { - display: none; -} - -.l7-popup-close-button:hover { - background-color: rgba(0, 0, 0, 0.05); -} - -.l7-popup-content { - position: relative; - background: #fff; - border-radius: 3px; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); - padding: 10px 10px 15px; - pointer-events: auto; -} - -/* general toolbar styles */ - -.l7-bar { - box-shadow: 0 0 1px rgba(0, 0, 0, 0.3); - border-radius: 4px; -} -.l7-bar a, -.l7-bar a:hover { - background-color: #fff; - width: 30px; - height: 30px; - font-size: 20px; - display: block; - text-align: center; - text-decoration: none; - color: #8e9dab; -} -.l7-bar a, -.l7-control-layers-toggle { - background-position: 50% 50%; - background-repeat: no-repeat; - display: block; -} -.l7-bar a:hover { - background-color: #f4f4f4; -} -.l7-bar a:first-child { - border-top-left-radius: 2px; - border-top-right-radius: 2px; -} -.l7-bar a:last-child { - border-bottom-left-radius: 2px; - border-bottom-right-radius: 2px; - border-bottom: none; -} -.l7-bar a.l7-disabled { - cursor: default; - background-color: #f4f4f4; - color: #bbb; -} - -/* control positioning */ - -.l7-control-container { - font: 12px/1.5 'Helvetica Neue', Arial, Helvetica, sans-serif; -} -.l7-control-hide { - display: none; -} -.l7-control { - position: relative; - z-index: 800; - pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ - pointer-events: auto; -} -.l7-control { - float: left; - clear: both; -} -.l7-top, -.l7-bottom { - position: absolute; - z-index: 1000; - pointer-events: none; -} -.l7-top { - top: 0; -} -.l7-right { - right: 0; -} -.l7-bottom { - bottom: 0; -} -.l7-left { - left: 0; -} -.l7-center { - display: flex; - position: absolute; - justify-content: center; -} -.l7-bottom.l7-center, -.l7-top.l7-center { - width: 100%; -} -.l7-right.l7-center, -.l7-left.l7-center { - height: 100%; - flex-direction: column; -} - -.l7-top.l7-center, -.l7-left.l7-center { - align-items: flex-start; -} - -.l7-bottom.l7-center, -.l7-right.l7-center { - align-items: flex-end; -} - -.l7-center .l7-control { - margin-right: 10px; - margin-bottom: 10px; -} - -.l7-control { - float: left; - clear: both; -} -.l7-right .l7-control { - float: right; -} -.l7-top .l7-control { - margin-top: 10px; -} -.l7-bottom .l7-control { - margin-bottom: 10px; -} -.l7-left .l7-control { - margin-left: 10px; -} -.l7-right .l7-control { - margin-right: 10px; -} - -/* attribution and scale controls */ - -.l7-control-container .l7-control-attribution { - background: #fff; - background: rgba(59, 58, 58, 0.7); - margin: 0; -} -.l7-control-attribution, -.l7-control-scale-line { - padding: 0 5px; - color: #333; -} -.l7-control-attribution a { - text-decoration: none; -} -.l7-control-attribution a:hover { - text-decoration: underline; -} -.l7-container .l7-control-attribution, -.l7-container .l7-control-scale { - font-size: 11px; - padding: 5px 5px 2px 5px; - background: rgba(255, 255, 255, 0.7); -} -.l7-left .l7-control-scale { - margin-left: 5px; -} -.l7-bottom .l7-control-scale { - margin-bottom: 5px; -} -.l7-control-scale-line { - border: 2px solid #000; - border-top: none; - color: #000; - line-height: 1.1; - padding: 2px 5px 1px; - font-size: 11px; - white-space: nowrap; - overflow: hidden; - -moz-box-sizing: border-box; - box-sizing: border-box; - - background: #fff; -} -.l7-control-scale-line:not(:first-child) { - border-top: 2px solid #777; - border-bottom: none; - margin-top: -2px; -} -.l7-control-scale-line:not(:first-child):not(:last-child) { - border-bottom: 2px solid #777; -} - -.l7-touch .l7-control-attribution, -.l7-touch .l7-control-layers, -.l7-touch .l7-bar { - box-shadow: none; -} -.l7-touch .l7-control-layers, -.l7-touch .l7-bar { - border: 2px solid rgba(0, 0, 0, 0.2); - background-clip: padding-box; -} -/*logo */ - -.l7-ctrl-logo { - background-size: 100% 100%; - width: 89px; - height: 16px; - margin: 0 0 -3px -3px; - display: flex; - background-repeat: no-repeat; - cursor: pointer; - background-image: url('data:image/png;base64,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'); -} - -/* layers control */ - -.l7-control-layers { - box-shadow: 0 1px 8px rgba(0, 0, 0, 0.4); - background: #fff; - border-radius: 2px; -} -.l7-control-layers-toggle { - background-image: url('data:image/png;base64,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'); - width: 30px; - height: 30px; - background-size: 20px 20px; -} - -.l7-touch .l7-control-layers-toggle { - width: 44px; - height: 44px; -} -.l7-control-layers .l7-control-layers-list, -.l7-control-layers-expanded .l7-control-layers-toggle { - display: none; -} -.l7-control-layers-expanded .l7-control-layers-list { - display: block; - position: relative; -} -.l7-control-layers-expanded { - padding: 6px 10px 6px 6px; - color: #59626b; - background: #fff; -} -.l7-control-layers-scrollbar { - overflow-y: scroll; - overflow-x: hidden; - padding-right: 5px; -} -.l7-control-layers-selector { - margin-top: 2px; - position: relative; - top: 1px; -} -.l7-control-layers label { - display: block; - padding: 8px; -} -.l7-control-layers label input[type='radio'], -.l7-control-layers label input[type='checkbox'] { - width: 14px; - height: 14px; - margin: 0; -} -.l7-control-layers-separator { - height: 0; - border-top: 1px solid #d8d8d8; - margin: 5px -10px 5px -6px; -} -.mapboxgl-ctrl-logo { - display: none !important; -} -.amap-logo { - display: none !important; -} diff --git a/packages/component/src/css/l7.less b/packages/component/src/css/l7.less new file mode 100644 index 0000000000..af01b556be --- /dev/null +++ b/packages/component/src/css/l7.less @@ -0,0 +1,60 @@ +.l7-marker-container { + position: absolute; + width: 100%; + height: 100%; + overflow: hidden; +} + +.l7-marker { + position: absolute !important; + top: 0; + left: 0; + z-index: 5; + cursor: pointer; +} + +.l7-marker-cluster { + width: 40px; + height: 40px; + background-color: rgba(181, 226, 140, 0.6); + background-clip: padding-box; + border-radius: 20px; +} +.l7-marker-cluster div { + width: 30px; + height: 30px; + margin-top: 5px; + margin-left: 5px; + font: 12px 'Helvetica Neue', Arial, Helvetica, sans-serif; + text-align: center; + background-color: rgba(110, 204, 57, 0.6); + border-radius: 15px; +} +.l7-marker-cluster span { + line-height: 30px; +} + +.l7-touch .l7-control-attribution, +.l7-touch .l7-control-layers, +.l7-touch .l7-bar { + box-shadow: none; +} +.l7-touch .l7-control-layers, +.l7-touch .l7-bar { + background-clip: padding-box; + border: 2px solid rgba(0, 0, 0, 0.2); +} + +// 隐藏底图 Logo +.mapboxgl-ctrl-logo, +.amap-logo { + display: none !important; +} + +.l7-select-box { + border: 3px dashed gray; + border-radius: 2px; + position: absolute; + z-index: 1000; + box-sizing: border-box; +} diff --git a/packages/component/src/css/layerPopup.less b/packages/component/src/css/layerPopup.less new file mode 100644 index 0000000000..08173a0f94 --- /dev/null +++ b/packages/component/src/css/layerPopup.less @@ -0,0 +1,8 @@ +@import 'variables'; + +.l7-layer-popup__row { + font-size: 12px; + & + & { + margin-top: 4px; + } +} diff --git a/packages/component/src/css/logo.less b/packages/component/src/css/logo.less new file mode 100644 index 0000000000..83a4130ed6 --- /dev/null +++ b/packages/component/src/css/logo.less @@ -0,0 +1,18 @@ +@import 'variables'; + +.l7-control-logo { + width: 89px; + height: 16px; + user-select: none; + img { + height: 100%; + width: 100%; + } + .l7-control-logo-link { + display: block; + cursor: pointer; + img { + cursor: pointer; + } + } +} diff --git a/packages/component/src/css/mouseLocation.less b/packages/component/src/css/mouseLocation.less new file mode 100644 index 0000000000..4c653a85d5 --- /dev/null +++ b/packages/component/src/css/mouseLocation.less @@ -0,0 +1,9 @@ +@import 'variables'; + +.l7-control-mouse-location { + background-color: @l7-control-bg-color; + border-radius: @l7-btn-control-border-radius; + box-shadow: @l7-control-shadow; + padding: 2px 4px; + min-width: 130px; +} diff --git a/packages/component/src/css/popper.less b/packages/component/src/css/popper.less new file mode 100644 index 0000000000..3179874e26 --- /dev/null +++ b/packages/component/src/css/popper.less @@ -0,0 +1,64 @@ +@import 'variables'; + +.l7-popper { + position: absolute; + display: flex; + justify-content: center; + align-items: center; + z-index: 5; + color: @l7-control-font-color; + &.l7-popper-hide { + display: none; + } + .l7-popper-content { + min-height: @l7-btn-control-size; + background: @l7-popper-control-bg-color; + border-radius: @l7-btn-control-border-radius; + box-shadow: @l7-control-shadow; + } + .l7-popper-arrow { + width: 0; + height: 0; + border-width: @l7-popper-control-arrow-size; + border-style: solid; + border-top-color: transparent; + border-bottom-color: transparent; + border-left-color: transparent; + border-right-color: transparent; + box-shadow: @l7-control-shadow; + } + &.l7-popper-left { + flex-direction: row; + .l7-popper-arrow { + border-left-color: @l7-popper-control-bg-color; + margin: (@l7-btn-control-size / 2 - @l7-popper-control-arrow-size) 0; + } + } + &.l7-popper-right { + flex-direction: row-reverse; + .l7-popper-arrow { + border-right-color: @l7-popper-control-bg-color; + margin: (@l7-btn-control-size / 2 - @l7-popper-control-arrow-size) 0; + } + } + &.l7-popper-top { + flex-direction: column; + .l7-popper-arrow { + border-top-color: @l7-popper-control-bg-color; + margin: 0 (@l7-btn-control-size / 2 - @l7-popper-control-arrow-size); + } + } + &.l7-popper-bottom { + flex-direction: column-reverse; + .l7-popper-arrow { + border-bottom-color: @l7-popper-control-bg-color; + margin: 0 (@l7-btn-control-size / 2 - @l7-popper-control-arrow-size); + } + } + &.l7-popper-start { + align-items: flex-start; + } + &.l7-popper-end { + align-items: flex-end; + } +} diff --git a/packages/component/src/css/popup.less b/packages/component/src/css/popup.less new file mode 100644 index 0000000000..59301379c4 --- /dev/null +++ b/packages/component/src/css/popup.less @@ -0,0 +1,169 @@ +@import 'variables'; + +.l7-popup { + position: absolute; + top: 0; + left: 0; + z-index: 5; + display: -webkit-flex; + display: flex; + pointer-events: none; + will-change: transform; + &.l7-popup-hide { + display: none; + } + + .l7-popup-content { + position: relative; + padding: 16px; + background: #fff; + border-radius: 3px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + pointer-events: auto; + font-size: 14px; + + .l7-popup-content__title { + font-weight: bold; + margin-bottom: 8px; + } + + .l7-popup-close-button { + position: absolute; + top: 0; + right: 0; + width: 18px; + height: 18px; + line-height: 18px; + text-align: center; + padding: 0; + background-color: transparent; + border: 0; + border-radius: 0 3px 0 0; + cursor: pointer; + font-size: 14px; + } + } + + .l7-popup-tip { + z-index: 1; + width: 0; + height: 0; + border: 10px solid transparent; + } + + &.l7-popup-anchor-bottom, + &.l7-popup-anchor-bottom-left, + &.l7-popup-anchor-bottom-right { + -webkit-flex-direction: column-reverse; + flex-direction: column-reverse; + } + + &.l7-popup-anchor-top, + &.l7-popup-anchor-top-left, + &.l7-popup-anchor-top-right { + -webkit-flex-direction: column; + flex-direction: column; + } + + &.l7-popup-anchor-left { + -webkit-flex-direction: row; + flex-direction: row; + } + + &.l7-popup-anchor-right { + -webkit-flex-direction: row-reverse; + flex-direction: row-reverse; + } +} + +.l7-popup-anchor-top .l7-popup-tip { + -webkit-align-self: center; + align-self: center; + border-top: none; + border-bottom-color: #fff; +} + +.l7-popup-anchor-top-left .l7-popup-tip { + -webkit-align-self: flex-start; + align-self: flex-start; + border-top: none; + border-bottom-color: #fff; + border-left: none; +} + +.l7-popup-anchor-top-right .l7-popup-tip { + -webkit-align-self: flex-end; + align-self: flex-end; + border-top: none; + border-right: none; + border-bottom-color: #fff; +} + +.l7-popup-anchor-bottom .l7-popup-tip { + -webkit-align-self: center; + align-self: center; + border-top-color: #fff; + border-bottom: none; +} + +.l7-popup-anchor-bottom-left .l7-popup-tip { + -webkit-align-self: flex-start; + align-self: flex-start; + border-top-color: #fff; + border-bottom: none; + border-left: none; +} + +.l7-popup-anchor-bottom-right .l7-popup-tip { + -webkit-align-self: flex-end; + align-self: flex-end; + border-top-color: #fff; + border-right: none; + border-bottom: none; +} + +.l7-popup-anchor-left .l7-popup-tip { + -webkit-align-self: center; + align-self: center; + border-right-color: #fff; + border-left: none; +} + +.l7-popup-anchor-right .l7-popup-tip { + -webkit-align-self: center; + align-self: center; + border-right: none; + border-left-color: #fff; +} + +.l7-popup-anchor-top-left .l7-popup-content { + border-top-left-radius: 0; +} + +.l7-popup-anchor-top-right .l7-popup-content { + border-top-right-radius: 0; +} + +.l7-popup-anchor-bottom-left .l7-popup-content { + border-bottom-left-radius: 0; +} + +.l7-popup-anchor-bottom-right .l7-popup-content { + border-bottom-right-radius: 0; +} + +.l7-popup-track-pointer { + display: none; + * { + user-select: none; + pointer-events: none; + } +} + +.l7-map:hover .l7-popup-track-pointer { + display: flex; +} + +.l7-map:active .l7-popup-track-pointer { + display: none; +} diff --git a/packages/component/src/css/scale.less b/packages/component/src/css/scale.less new file mode 100644 index 0000000000..a280e4ee7d --- /dev/null +++ b/packages/component/src/css/scale.less @@ -0,0 +1,34 @@ +@import 'variables'; + +.l7-control-scale { + display: flex; + flex-direction: column; + .l7-control-scale-line { + box-sizing: border-box; + padding: 2px 5px 1px; + overflow: hidden; + color: @l7-control-font-color; + font-size: 10px; + line-height: 1.1; + white-space: nowrap; + background: @l7-control-bg-color; + border: 2px solid #000; + border-top: 0; + transition: width 0.1s; + & + & { + margin-top: -2px; + border-top: 2px solid #777; + border-bottom: none; + } + } +} + +.l7-right { + .l7-control-scale { + display: flex; + align-items: flex-end; + .l7-control-scale-line { + text-align: right; + } + } +} diff --git a/packages/component/src/css/select.less b/packages/component/src/css/select.less new file mode 100644 index 0000000000..00d6353f72 --- /dev/null +++ b/packages/component/src/css/select.less @@ -0,0 +1,86 @@ +@import 'variables'; + +.l7-select-control--normal { + padding: 4px 0; + .l7-select-control-item { + height: 24px; + line-height: 24px; + display: flex; + align-items: center; + padding: 0 16px; + font-size: 12px; + > * + * { + margin-left: 6px; + } + input[type='checkbox'] { + height: 14px; + width: 14px; + } + &:hover { + background-color: @l7-btn-control-bg-hover-color; + } + } +} + +.l7-select-control--image { + padding: 12px 12px 0 12px; + width: @l7-select-control-image-popper-width; + height: 320px; + overflow: auto; + display: flex; + flex-wrap: wrap; + box-sizing: border-box; + align-items: flex-start; + .l7-select-control-item { + margin-right: 12px; + border-radius: @l7-btn-control-border-radius; + overflow: hidden; + border: 1px solid @l7-popper-control-bg-color; + box-sizing: content-box; + width: calc((100% - 36px) / 3); + display: flex; + flex-direction: column; + justify-content: center; + margin-bottom: 12px; + position: relative; + font-size: 12px; + img { + width: 142px; + height: 80px; + } + input[type='checkbox'] { + position: absolute; + right: 0; + top: 0; + } + .l7-select-control-item-row { + display: flex; + justify-content: center; + align-items: center; + line-height: 26px; + > * + * { + margin-left: 8px; + } + } + &.l7-select-control-item-active { + border-color: @l7-select-control-active-color; + } + &:nth-child(3n) { + margin-right: 0; + } + } +} + +.l7-select-control-item { + cursor: pointer; + input[type='checkbox'] { + margin: 0; + cursor: pointer; + } +} + +.l7-select-control--multiple { + .l7-select-control-item:hover { + background-color: transparent; + } +} diff --git a/packages/component/src/css/variables.less b/packages/component/src/css/variables.less new file mode 100644 index 0000000000..e587e44b4f --- /dev/null +++ b/packages/component/src/css/variables.less @@ -0,0 +1,28 @@ +// Control +@l7-control-space: 8px; +@l7-control-font-size: 12px; +@l7-control-font-color: #595959; +@l7-control-bg-color: #fff; +@l7-control-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.15); + +// ButtonControl +@l7-btn-control-bg-color: @l7-control-bg-color; +@l7-btn-control-bg-hover-color: #f3f3f3; +@l7-btn-control-bg-active-color: @l7-btn-control-bg-hover-color; +@l7-btn-control-size: 28px; +@l7-btn-icon-size: 16px; +@l7-btn-control-border-radius: 2px; +@l7-btn-control-disabled-bg-color: #fafafa; +@l7-btn-control-disabled-font-color: #bdbdbd; +@l7-btn-border-color: #f0f0f0; +@l7-btn-column-height: 44px; + +// PopperControl +@l7-popper-control-bg-color: @l7-btn-control-bg-color; +@l7-popper-control-arrow-size: 4px; + +// SelectControl +@l7-select-control-active-color: #0370fe; +@l7-select-control-image-popper-width: 474px; + +@position-list: top, right, bottom, left; diff --git a/packages/component/src/css/zoom.less b/packages/component/src/css/zoom.less new file mode 100644 index 0000000000..6ef139d429 --- /dev/null +++ b/packages/component/src/css/zoom.less @@ -0,0 +1,21 @@ +@import 'variables'; + +@zoom-icon-size: 14px; + +.l7-control-zoom { + box-shadow: @l7-control-shadow; + border-radius: @l7-btn-control-border-radius; + overflow: hidden; + .l7-button-control { + box-shadow: 0 0 0; + border-radius: 0; + font-size: @l7-btn-icon-size; + .l7-iconfont { + width: @zoom-icon-size; + height: @zoom-icon-size; + } + &:first-child { + border-bottom: 1px solid @l7-btn-border-color; + } + } +} diff --git a/packages/component/src/images/logo.png b/packages/component/src/images/logo.png index 7d72ce2343..b2e6d85be9 100644 Binary files a/packages/component/src/images/logo.png and b/packages/component/src/images/logo.png differ diff --git a/packages/component/src/images/quanping.svg b/packages/component/src/images/quanping.svg new file mode 100644 index 0000000000..4d116a5724 --- /dev/null +++ b/packages/component/src/images/quanping.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/packages/component/src/index.ts b/packages/component/src/index.ts index a7508dfbf9..ee26862a9b 100644 --- a/packages/component/src/index.ts +++ b/packages/component/src/index.ts @@ -1,16 +1,23 @@ -import Control from './control/BaseControl'; -import Layers from './control/layer'; -import Logo from './control/logo'; -import Scale from './control/scale'; -import Zoom from './control/zoom'; import Marker from './marker'; import MarkerLayer from './marker-layer'; -import Popup from './popup'; +import './assets/iconfont/iconfont.js'; // 引入样式 -// TODO: 使用 Less 或者 Sass,每个组件单独引用自身样式 -import './css/l7.css'; +import './css/index.css'; -export { Control, Logo, Scale, Zoom, Layers, Marker, Popup, MarkerLayer }; +export * from './control/baseControl'; +export * from './control/logo'; +export * from './control/fullscreen'; +export * from './control/exportImage'; +export * from './control/geoLocate'; +export * from './control/mapTheme'; +export * from './control/layerControl'; +export * from './control/mouseLocation'; +export * from './control/zoom'; +export * from './control/scale'; +export * from './popup/popup'; +export * from './popup/layerPopup'; + +export { Marker, MarkerLayer }; export * from './interface'; diff --git a/packages/component/src/interface.ts b/packages/component/src/interface.ts index 9ff8558021..9e4e197444 100644 --- a/packages/component/src/interface.ts +++ b/packages/component/src/interface.ts @@ -1,27 +1,4 @@ -import { IControlOption } from '@antv/l7-core'; - -export interface ILayerControlOption extends IControlOption { - collapsed: boolean; - autoZIndex: boolean; - hideSingleBase: boolean; - sortLayers: boolean; - - sortFunction: (...args: any[]) => any; -} - -export interface IScaleControlOption extends IControlOption { - maxWidth: number; - metric: boolean; - updateWhenIdle: boolean; - imperial: boolean; -} - -export interface IZoomControlOption extends IControlOption { - zoomInText: string; - zoomInTitle: string; - zoomOutText: string; - zoomOutTitle: string; -} +export type ControlEvent = 'show' | 'hide' | 'add' | 'remove' | string; export interface IMarkerStyleOption { element?: (...args: any[]) => any; diff --git a/packages/component/src/popup.ts b/packages/component/src/popup.ts deleted file mode 100644 index 341bae97a7..0000000000 --- a/packages/component/src/popup.ts +++ /dev/null @@ -1,262 +0,0 @@ -import { - ILngLat, - IMapService, - IPopup, - IPopupOption, - ISceneService, - TYPES, -} from '@antv/l7-core'; -import { - anchorTranslate, - anchorType, - applyAnchorClass, - bindAll, - DOM, -} from '@antv/l7-utils'; -import { EventEmitter } from 'eventemitter3'; -import { Container } from 'inversify'; - -/** colse event */ - -export default class Popup extends EventEmitter implements IPopup { - private popupOption: IPopupOption; - private mapsService: IMapService; - private sceneSerive: ISceneService; - private lngLat: ILngLat; - private content: HTMLElement; - private closeButton: HTMLElement; - private timeoutInstance: any; - private container: HTMLElement; - private tip: HTMLElement; - private scene: Container; - - constructor(cfg?: Partial) { - super(); - this.popupOption = { - ...this.getdefault(), - ...cfg, - }; - bindAll(['update', 'onClickClose', 'remove'], this); - } - - public addTo(scene: Container) { - this.mapsService = scene.get(TYPES.IMapService); - this.sceneSerive = scene.get(TYPES.ISceneService); - this.mapsService.on('camerachange', this.update); - this.mapsService.on('viewchange', this.update); - this.scene = scene; - this.update(); - if (this.popupOption.closeOnClick) { - this.timeoutInstance = setTimeout(() => { - this.mapsService.on('click', this.onClickClose); - }, 30); - } - this.emit('open'); - return this; - } - - public close(): void { - this.remove(); - } - - public open(): void { - this.addTo(this.scene); - } - - public setHTML(html: string) { - const frag = window.document.createDocumentFragment(); - const temp = window.document.createElement('body'); - let child: ChildNode | null; - temp.innerHTML = html; - while (true) { - child = temp.firstChild; - if (!child) { - break; - } - frag.appendChild(child); - } - - return this.setDOMContent(frag); - } - - public setLnglat(lngLat: ILngLat | number[]): this { - this.lngLat = lngLat as ILngLat; - if (Array.isArray(lngLat)) { - this.lngLat = { - lng: lngLat[0], - lat: lngLat[1], - }; - } - if (this.mapsService) { - this.mapsService.on('camerachange', this.update); - this.mapsService.on('viewchange', this.update); - } - this.update(); - return this; - } - public getLnglat(): ILngLat { - return this.lngLat; - } - public setText(text: string) { - return this.setDOMContent(window.document.createTextNode(text)); - } - - public setMaxWidth(maxWidth: string): this { - this.popupOption.maxWidth = maxWidth; - this.update(); - return this; - } - - public setDOMContent(htmlNode: ChildNode | DocumentFragment) { - this.createContent(); - this.content.appendChild(htmlNode); - this.update(); - return this; - } - - // 移除popup - public remove() { - if (this.content) { - this.removeDom(this.content); - } - - if (this.container) { - this.removeDom(this.container); - // @ts-ignore - delete this.container; - } - if (this.mapsService) { - // TODO: mapbox AMap 事件同步 - this.mapsService.off('camerachange', this.update); - this.mapsService.off('viewchange', this.update); - this.mapsService.off('click', this.onClickClose); - // @ts-ignore - delete this.mapsService; - } - clearTimeout(this.timeoutInstance); - this.emit('close'); - return this; - } - public isOpen() { - return !!this.mapsService; - } - - private createContent() { - if (this.content) { - DOM.remove(this.content); - } - this.content = DOM.create('div', 'l7-popup-content', this.container); - if (this.popupOption.closeButton) { - this.closeButton = DOM.create( - 'button', - 'l7-popup-close-button', - this.content, - ); - - if (this.popupOption.closeButtonOffsets) { - // 关闭按钮的偏移 - this.closeButton.style.right = - this.popupOption.closeButtonOffsets[0] + 'px'; - this.closeButton.style.top = - this.popupOption.closeButtonOffsets[1] + 'px'; - } - - // this.closeButton.type = 'button'; - this.closeButton.setAttribute('aria-label', 'Close popup'); - this.closeButton.innerHTML = '×'; - this.closeButton.addEventListener('click', this.onClickClose); - } - } - - private creatDom(tagName: string, className: string, container: HTMLElement) { - const el = window.document.createElement(tagName); - if (className !== undefined) { - el.className = className; - } - if (container) { - container.appendChild(el); - } - return el; - } - - private removeDom(node: ChildNode) { - if (node.parentNode) { - node.parentNode.removeChild(node); - } - } - - private getdefault() { - return { - closeButton: true, - closeOnClick: true, - maxWidth: '240px', - offsets: [0, 0], - anchor: anchorType.BOTTOM, - className: '', - stopPropagation: true, - }; - } - - private onClickClose(e: Event) { - if (e.stopPropagation) { - e.stopPropagation(); - } - this.remove(); - } - - private update() { - const hasPosition = this.lngLat; - const { className, maxWidth, anchor } = this.popupOption; - if (!this.mapsService || !hasPosition || !this.content) { - return; - } - const popupContainer = this.mapsService.getMarkerContainer(); - if (!this.container && popupContainer) { - this.container = this.creatDom( - 'div', - 'l7-popup', - popupContainer as HTMLElement, - ); - - this.tip = this.creatDom('div', 'l7-popup-tip', this.container); - this.container.appendChild(this.content); - if (className) { - className - .split(' ') - .forEach((name) => this.container.classList.add(name)); - } - - // 高德地图需要阻止事件冒泡 // 测试mapbox 地图不需要添加 - const { stopPropagation } = this.popupOption; - if (stopPropagation) { - ['mousemove', 'mousedown', 'mouseup', 'click', 'dblclick'].forEach( - (type) => { - this.container.addEventListener(type, (e) => { - e.stopPropagation(); - }); - }, - ); - } - - this.container.style.whiteSpace = 'nowrap'; - } - if (maxWidth && this.container.style.maxWidth !== maxWidth) { - this.container.style.maxWidth = maxWidth; - } - - this.updatePosition(); - DOM.setTransform(this.container, `${anchorTranslate[anchor]}`); - applyAnchorClass(this.container, anchor, 'popup'); - } - - private updatePosition() { - if (!this.mapsService) { - return; - } - const { lng, lat } = this.lngLat; - const { offsets } = this.popupOption; - const pos = this.mapsService.lngLatToContainer([lng, lat]); - this.container.style.left = pos.x + offsets[0] + 'px'; - this.container.style.top = pos.y - offsets[1] + 'px'; - } -} diff --git a/packages/component/src/popup/layerPopup.ts b/packages/component/src/popup/layerPopup.ts new file mode 100644 index 0000000000..b4295a6ba5 --- /dev/null +++ b/packages/component/src/popup/layerPopup.ts @@ -0,0 +1,269 @@ +import { ILayer, IPopupOption } from '@antv/l7-core'; +// @ts-ignore +// tslint:disable-next-line:no-implicit-dependencies +import { BaseLayer } from '@antv/l7-layers'; +import { DOM } from '@antv/l7-utils'; +import { Container } from 'inversify'; +import { get } from 'lodash'; +// import { Container } from 'inversify'; +import Popup from './popup'; + +export type LayerField = { + field: string; + formatField?: (field: string) => string; + formatValue?: (value: any) => any; + getValue?: (feature: any) => any; +}; + +export type LayerPopupConfigItem = { + layer: BaseLayer | string; + fields: Array; +}; + +export interface ILayerPopupOption extends IPopupOption { + config: LayerPopupConfigItem[]; + trigger: 'hover' | 'click'; +} + +type LayerMapInfo = { + onMouseMove?: (layer: BaseLayer, e: any) => void; + onMouseOut?: (layer: BaseLayer, e: any) => void; + onClick?: (layer: BaseLayer, e: any) => void; + onSourceUpdate?: (layer: BaseLayer) => void; +} & Partial; + +export { LayerPopup }; + +export default class LayerPopup extends Popup { + /** + * 用于保存图层对应的事件回调以及配置信息 + * @protected + */ + protected layerConfigMap: WeakMap = new WeakMap(); + + /** + * 当期正在展示的图层以及对应元素 id 的信息 + * @protected + */ + protected displayFeatureInfo?: { + layer: ILayer; + featureId: number; + }; + + public addTo(scene: Container) { + super.addTo(scene); + this.bindLayerEvent(); + this.hide(); + return this; + } + + public remove() { + super.remove(); + this.unbindLayerEvent(); + return this; + } + + public setOptions(option: Partial) { + this.unbindLayerEvent(); + super.setOptions(option); + this.bindLayerEvent(); + return this; + } + + protected getDefault(option: Partial): ILayerPopupOption { + const isClickTrigger = option.trigger === 'click'; + + return { + ...super.getDefault(option), + trigger: 'hover', + followCursor: !isClickTrigger, + lngLat: { + lng: 0, + lat: 0, + }, + offsets: [0, 10], + closeButton: false, + closeOnClick: false, + autoClose: false, + closeOnEsc: false, + }; + } + + /** + * 绑定对应的图层事件 + * @protected + */ + protected bindLayerEvent() { + const { config, trigger } = this.popupOption; + config.forEach((configItem) => { + const layer = this.getLayerByConfig(configItem); + if (!layer) { + return; + } + const layerInfo: LayerMapInfo = { + ...configItem, + }; + + if (trigger === 'hover') { + const onMouseMove = this.onLayerMouseMove.bind(this, layer); + const onMouseOut = this.onLayerMouseOut.bind(this, layer); + layerInfo.onMouseMove = onMouseMove; + layerInfo.onMouseOut = onMouseOut; + + layer?.on('mousemove', onMouseMove); + layer?.on('mouseout', onMouseOut); + } else { + const onClick = this.onLayerClick.bind(this, layer); + layerInfo.onClick = onClick; + + layer?.on('click', onClick); + } + const source = layer.getSource(); + const onSourceUpdate = this.onSourceUpdate.bind(this, layer); + source?.on('update', onSourceUpdate); + layerInfo.onSourceUpdate = onSourceUpdate; + + this.layerConfigMap.set(layer, layerInfo); + }); + } + + /** + * 解绑对应的图层事件 + * @protected + */ + protected unbindLayerEvent() { + const { config } = this.popupOption; + config.forEach((configItem) => { + const layer = this.getLayerByConfig(configItem); + const layerInfo = layer && this.layerConfigMap.get(layer); + if (!layerInfo) { + return; + } + const { onMouseMove, onMouseOut, onClick, onSourceUpdate } = layerInfo; + if (onMouseMove) { + layer.off('mousemove', onMouseMove); + } + if (onMouseOut) { + layer.off('mouseout', onMouseOut); + } + if (onClick) { + layer.off('click', onClick); + } + if (onSourceUpdate) { + layer?.getSource()?.off('update', onSourceUpdate); + } + }); + } + + protected onLayerMouseMove(layer: ILayer, e: any) { + if (!this.isSameFeature(layer, e.featureId)) { + const frag = this.getLayerInfoFrag(layer, e); + this.setDOMContent(frag); + this.displayFeatureInfo = { + layer, + featureId: e.featureId, + }; + } + + if (!this.isShow) { + this.show(); + } + } + + protected onLayerMouseOut(layer: ILayer, e: any) { + this.displayFeatureInfo = undefined; + if (this.isShow) { + this.hide(); + } + } + + protected onLayerClick(layer: ILayer, e: any) { + if (this.isShow && this.isSameFeature(layer, e.featureId)) { + this.hide(); + } else { + const frag = this.getLayerInfoFrag(layer, e); + this.setDOMContent(frag); + this.setLnglat(e.lngLat); + this.show(); + this.displayFeatureInfo = { + layer, + featureId: e.featureId, + }; + } + } + + protected onSourceUpdate(layer: ILayer) { + if (this.displayFeatureInfo?.layer === layer) { + this.hide(); + this.displayFeatureInfo = undefined; + } + } + + /** + * 通过当前图层和对应选中的元素获取气泡展示的 HTML 内容 + * @param layer + * @param e + * @protected + */ + protected getLayerInfoFrag(layer: ILayer, e: any): DocumentFragment { + const layerInfo = this.layerConfigMap.get(layer); + const frag = document.createDocumentFragment(); + if (layerInfo) { + let feature = e.feature; + if ( + feature.type === 'Feature' && + 'properties' in feature && + 'geometry' in feature + ) { + feature = feature.properties; + } + const { fields } = layerInfo; + fields?.forEach((fieldConfig) => { + const { field, formatField, formatValue, getValue } = + typeof fieldConfig === 'string' + ? ({ field: fieldConfig } as any) + : fieldConfig; + const row = DOM.create('div', 'l7-layer-popup__row'); + const value = getValue ? getValue(e.feature) : get(feature, field); + row.innerHTML = `${formatField ? formatField(field) : field}: ${ + formatValue ? formatValue(value) : value + }`; + frag.appendChild(row); + }); + } + return frag; + } + + /** + * 通过 Layer 配置访问到真实的 Layer 实例 + * @param config + * @protected + */ + protected getLayerByConfig(config: LayerPopupConfigItem): ILayer | undefined { + const layer = config.layer; + if (layer instanceof Object) { + return layer; + } + if (typeof layer === 'string') { + return ( + this.layerService.getLayer(layer) || + this.layerService.getLayerByName(layer) + ); + } + } + + /** + * 判断当前展示的 Feature 是否和上一次查看的一致 + * @param layer + * @param featureId + * @protected + */ + protected isSameFeature(layer: ILayer, featureId: number) { + const displayFeatureInfo = this.displayFeatureInfo; + return ( + displayFeatureInfo && + layer === displayFeatureInfo.layer && + featureId === displayFeatureInfo.featureId + ); + } +} diff --git a/packages/component/src/popup/popup.ts b/packages/component/src/popup/popup.ts new file mode 100644 index 0000000000..5202ce09e1 --- /dev/null +++ b/packages/component/src/popup/popup.ts @@ -0,0 +1,575 @@ +import { + ILayerService, + ILngLat, + IMapService, + IPopup, + IPopupOption, + ISceneService, + PopupHTML, + TYPES, +} from '@antv/l7-core'; +import { + anchorTranslate, + anchorType, + applyAnchorClass, + DOM, +} from '@antv/l7-utils'; +import { EventEmitter } from 'eventemitter3'; +import { Container } from 'inversify'; +import { createL7Icon } from '../utils/icon'; + +export { Popup }; + +export default class Popup + extends EventEmitter + implements IPopup { + /** + * 配置 + * @protected + */ + protected popupOption: O; + protected mapsService: IMapService; + protected sceneService: ISceneService; + protected layerService: ILayerService; + protected scene: Container; + + /** + * 关闭按钮对应的 DOM + * @protected + */ + protected closeButton?: HTMLElement | SVGElement; + + /** + * Popup 的总容器 DOM,包含 content 和 tip + * @protected + */ + protected container: HTMLElement; + + /** + * popup 气泡容器 + * @protected + */ + protected content: HTMLElement; + + /** + * popup 气泡标题 + * @protected + */ + protected contentTitle?: HTMLElement; + + /** + * popup 内容容器 + * @protected + */ + protected contentPanel: HTMLElement; + + /** + * popup 内容标题 + * @protected + */ + protected title: HTMLElement; + + /** + * 气泡箭头对应的 DOM + * @protected + */ + protected tip: HTMLElement; + + /** + * 当前是否展示 + * @protected + */ + protected isShow: boolean = true; + + protected get lngLat() { + return ( + this.popupOption.lngLat ?? { + lng: 0, + lat: 0, + } + ); + } + + protected set lngLat(newLngLat: ILngLat) { + this.popupOption.lngLat = newLngLat; + } + + constructor(cfg?: Partial) { + super(); + this.popupOption = { + ...this.getDefault(cfg ?? {}), + ...cfg, + }; + const { lngLat } = this.popupOption; + if (lngLat) { + this.lngLat = lngLat; + } + } + + public getIsShow() { + return this.isShow; + } + + public addTo(scene: Container) { + this.mapsService = scene.get(TYPES.IMapService); + this.sceneService = scene.get(TYPES.ISceneService); + this.layerService = scene.get(TYPES.ILayerService); + this.mapsService.on('camerachange', this.update); + this.mapsService.on('viewchange', this.update); + this.scene = scene; + this.update(); + + this.updateCloseOnClick(); + this.updateCloseOnEsc(); + this.updateFollowCursor(); + + const { html, text } = this.popupOption; + if (html) { + this.setHTML(html); + } else if (text) { + this.setText(text); + } + this.emit('open'); + return this; + } + + // 移除popup + public remove() { + if (!this.isOpen()) { + return; + } + + if (this.content) { + DOM.remove(this.content); + } + + if (this.container) { + DOM.remove(this.container); + // @ts-ignore + delete this.container; + } + if (this.mapsService) { + // TODO: mapbox AMap 事件同步 + this.mapsService.off('camerachange', this.update); + this.mapsService.off('viewchange', this.update); + this.updateCloseOnClick(true); + this.updateCloseOnEsc(true); + this.updateFollowCursor(true); + // @ts-ignore + delete this.mapsService; + } + this.emit('close'); + return this; + } + + /** + * 获取 option 配置 + */ + public getOptions() { + return this.popupOption; + } + + public setOptions(option: Partial) { + this.popupOption = { + ...this.popupOption, + ...option, + }; + if ( + this.checkUpdateOption(option, [ + 'closeButton', + 'closeButtonOffsets', + 'maxWidth', + 'anchor', + 'stopPropagation', + 'className', + 'style', + 'lngLat', + 'offsets', + 'title', + ]) + ) { + if (this.container) { + DOM.remove(this.container); + // @ts-ignore + this.container = undefined; + } + if (this.popupOption.html) { + this.setHTML(this.popupOption.html); + } else if (this.popupOption.text) { + this.setText(this.popupOption.text); + } + } + if (this.checkUpdateOption(option, ['closeOnEsc'])) { + this.updateCloseOnEsc(); + } + if (this.checkUpdateOption(option, ['closeOnClick'])) { + this.updateCloseOnClick(); + } + if (this.checkUpdateOption(option, ['followCursor'])) { + this.updateFollowCursor(); + } + if (this.checkUpdateOption(option, ['html']) && option.html) { + this.setHTML(option.html); + } else if (this.checkUpdateOption(option, ['text']) && option.text) { + this.setText(option.text); + } + if (this.checkUpdateOption(option, ['lngLat']) && option.lngLat) { + this.setLnglat(option.lngLat); + } + return this; + } + + public open() { + this.addTo(this.scene); + return this; + } + + public close() { + this.remove(); + return this; + } + + public show() { + if (this.isShow) { + return; + } + if (this.container) { + DOM.removeClass(this.container, 'l7-popup-hide'); + } + this.isShow = true; + this.emit('show'); + return this; + } + + public hide() { + if (!this.isShow) { + return; + } + if (this.container) { + DOM.addClass(this.container, 'l7-popup-hide'); + } + this.isShow = false; + this.emit('hide'); + return this; + } + + /** + * 设置 HTML 内容 + * @param html + */ + public setHTML(html: PopupHTML) { + this.popupOption.html = html; + return this.setDOMContent(this.getPopupHTMLFragment(html)); + } + + /** + * 设置 Popup 展示文本 + * @param text + */ + public setText(text: string) { + this.popupOption.text = text; + return this.setDOMContent(window.document.createTextNode(text)); + } + + /** + * 将地图自动平移到气泡位置 + */ + public panToPopup() { + const { lng, lat } = this.lngLat; + if (this.popupOption.autoPan) { + this.mapsService.panTo([lng, lat]); + } + return this; + } + + public setLngLat(lngLat: ILngLat | [number, number]): this { + return this.setLnglat(lngLat); + } + + /** + * 设置 Popup 所在经纬度 + * @param lngLat + */ + public setLnglat(lngLat: ILngLat | [number, number]): this { + this.lngLat = lngLat as ILngLat; + if (Array.isArray(lngLat)) { + this.lngLat = { + lng: lngLat[0], + lat: lngLat[1], + }; + } + if (this.mapsService) { + // 防止事件重复监听 + this.mapsService.off('camerachange', this.update); + this.mapsService.off('viewchange', this.update); + + this.mapsService.on('camerachange', this.update); + this.mapsService.on('viewchange', this.update); + } + this.update(); + if (this.popupOption.autoPan) { + setTimeout(() => { + this.panToPopup(); + }, 0); + } + return this; + } + + /** + * 获取 Popup 所在经纬度 + */ + public getLnglat(): ILngLat { + return this.lngLat; + } + + /** + * 设置 Popup 最大宽度 + * @param maxWidth + */ + public setMaxWidth(maxWidth: string): this { + this.popupOption.maxWidth = maxWidth; + this.update(); + return this; + } + + public isOpen() { + return !!this.mapsService; + } + + protected onMouseMove = (e: MouseEvent) => { + const container = this.mapsService.getMapContainer(); + const { left = 0, top = 0 } = container?.getBoundingClientRect() ?? {}; + this.setPopupPosition(e.clientX - left, e.clientY - top); + }; + + /** + * 将经纬度转换成对应的像素偏移位置 + * @protected + */ + protected updateLngLatPosition = () => { + if (!this.mapsService || this.popupOption.followCursor) { + return; + } + const { lng, lat } = this.lngLat; + const { x, y } = this.mapsService.lngLatToContainer([lng, lat]); + this.setPopupPosition(x, y); + }; + + protected getDefault(option: Partial): O { + // tslint:disable-next-line:no-object-literal-type-assertion + return { + closeButton: true, + closeOnClick: true, + maxWidth: '240px', + offsets: [0, 0], + anchor: anchorType.BOTTOM, + stopPropagation: true, + autoPan: false, + autoClose: true, + closeOnEsc: false, + followCursor: false, + } as O; + } + + /** + * 设置 Popup 内容 HTML + * @param htmlNode + */ + protected setDOMContent(htmlNode: ChildNode | DocumentFragment) { + this.createContent(); + this.contentPanel.appendChild(htmlNode); + this.update(); + return this; + } + + /** + * 绑定地图点击事件触发销毁 Popup + * @protected + */ + protected updateCloseOnClick(onlyClear?: boolean) { + this.mapsService.off('click', this.onCloseButtonClick); + if (this.popupOption.closeOnClick && !onlyClear) { + this.mapsService.on('click', this.onCloseButtonClick); + } + } + + protected updateCloseOnEsc(onlyClear?: boolean) { + window.removeEventListener('keydown', this.onKeyDown); + if (this.popupOption.closeOnEsc && !onlyClear) { + window.addEventListener('keydown', this.onKeyDown); + } + } + + protected updateFollowCursor(onlyClear?: boolean) { + const container = this.mapsService.getContainer()!; + container.removeEventListener('mousemove', this.onMouseMove); + if (this.popupOption.followCursor && !onlyClear) { + container.addEventListener('mousemove', this.onMouseMove); + } + } + + protected onKeyDown = (e: KeyboardEvent) => { + if (e.keyCode === 27) { + this.remove(); + } + }; + + /** + * 创建 Popup 内容容器的 DOM (在每次 setHTML 或 setText 时都会被调用) + * @protected + */ + protected createContent() { + if (this.content) { + DOM.remove(this.content); + } + this.content = DOM.create('div', 'l7-popup-content', this.container); + + if (this.popupOption.title) { + this.contentTitle = DOM.create( + 'div', + 'l7-popup-content__title', + this.content, + ); + this.contentTitle?.append( + this.getPopupHTMLFragment(this.popupOption.title), + ); + } else { + this.contentTitle = undefined; + } + + if (this.popupOption.closeButton) { + const closeButton = createL7Icon('l7-icon-guanbi'); + DOM.addClass(closeButton, 'l7-popup-close-button'); + this.content.appendChild(closeButton); + + if (this.popupOption.closeButtonOffsets) { + // 关闭按钮的偏移 + closeButton.style.right = this.popupOption.closeButtonOffsets[0] + 'px'; + closeButton.style.top = this.popupOption.closeButtonOffsets[1] + 'px'; + } + + // this.closeButton.type = 'button'; + closeButton.setAttribute('aria-label', 'Close popup'); + closeButton.addEventListener('click', this.onCloseButtonClick); + + this.closeButton = closeButton; + } else { + this.closeButton = undefined; + } + + this.contentPanel = DOM.create( + 'div', + 'l7-popup-content__panel', + this.content, + ); + } + + protected onCloseButtonClick = (e: Event) => { + if (e.stopPropagation) { + e.stopPropagation(); + } + this.remove(); + }; + + protected update = () => { + const hasPosition = !!this.lngLat; + const { + className, + style, + maxWidth, + anchor, + stopPropagation, + } = this.popupOption; + if (!this.mapsService || !hasPosition || !this.content) { + return; + } + const popupContainer = this.mapsService.getMarkerContainer(); + // 如果当前没有创建 Popup 容器则创建 + if (!this.container && popupContainer) { + this.container = DOM.create( + 'div', + `l7-popup ${className ?? ''} ${!this.isShow ? 'l7-popup-hide' : ''}`, + popupContainer as HTMLElement, + ); + + if (style) { + this.container.setAttribute('style', style); + } + + this.tip = DOM.create('div', 'l7-popup-tip', this.container); + this.container.appendChild(this.content); + + // 高德地图需要阻止事件冒泡 // 测试mapbox 地图不需要添加 + if (stopPropagation) { + ['mousemove', 'mousedown', 'mouseup', 'click', 'dblclick'].forEach( + (type) => { + this.container.addEventListener(type, (e) => { + e.stopPropagation(); + }); + }, + ); + } + + this.container.style.whiteSpace = 'nowrap'; + } + + // 设置 Popup 的最大宽度 + if (maxWidth && this.container.style.maxWidth !== maxWidth) { + this.container.style.maxWidth = maxWidth; + } + + this.updateLngLatPosition(); + DOM.setTransform(this.container, `${anchorTranslate[anchor]}`); + applyAnchorClass(this.container, anchor, 'popup'); + }; + + /** + * 设置 Popup 相对于地图容器的 Position + * @param left + * @param top + * @protected + */ + protected setPopupPosition(left: number, top: number) { + if (this.container) { + const { offsets } = this.popupOption; + this.container.style.left = left + offsets[0] + 'px'; + this.container.style.top = top - offsets[1] + 'px'; + } + } + + /** + * 检查当前传入 option 是否包含 keys 字段 + * @param option + * @param keys + * @protected + */ + protected checkUpdateOption(option: Partial, keys: Array) { + return keys.some((key) => key in option); + } + + /** + * 根据参数 HTML 片段返回对应的 Fragment + * @param html + * @protected + */ + protected getPopupHTMLFragment(html: PopupHTML) { + const frag = window.document.createDocumentFragment(); + const temp = window.document.createElement('body'); + let child: ChildNode | null; + if (typeof html === 'string') { + temp.innerHTML = html; + } else if (Array.isArray(html)) { + temp.append(...html); + } else if (html instanceof HTMLElement) { + temp.append(html); + } + while (true) { + child = temp.firstChild; + if (!child) { + break; + } + frag.appendChild(child); + } + return frag; + } +} diff --git a/packages/component/src/utils/icon.ts b/packages/component/src/utils/icon.ts new file mode 100644 index 0000000000..fbfa9b91b2 --- /dev/null +++ b/packages/component/src/utils/icon.ts @@ -0,0 +1,9 @@ +export const createL7Icon = (className: string) => { + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.classList.add('l7-iconfont'); + svg.setAttribute('aria-hidden', 'true'); + const use = document.createElementNS('http://www.w3.org/2000/svg', 'use'); + use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', `#${className}`); + svg.appendChild(use); + return svg; +}; diff --git a/packages/component/src/utils/popper.ts b/packages/component/src/utils/popper.ts new file mode 100644 index 0000000000..ec40f6647b --- /dev/null +++ b/packages/component/src/utils/popper.ts @@ -0,0 +1,286 @@ +import { DOM } from '@antv/l7-utils'; +import { EventEmitter } from 'eventemitter3'; + +/** + * 气泡位置枚举 + */ +export type PopperPlacement = + | 'top-start' + | 'top' + | 'top-end' + | 'left-start' + | 'left' + | 'left-end' + | 'bottom-start' + | 'bottom' + | 'bottom-end' + | 'right-start' + | 'right' + | 'right-end'; + +/** + * 气泡触发类型,当前支持 click 和 hover 两种类型 + */ +export type PopperTrigger = 'click' | 'hover'; + +/** + * 气泡内容类型 + */ +export type PopperContent = string | HTMLElement | null; + +export interface IPopperOption { + placement: PopperPlacement; // 气泡展示方向 + trigger: PopperTrigger; // 气泡触发方式 + content?: PopperContent; // 初始内容 + offset?: [number, number]; // 气泡偏移 + className?: string; // 容器自定义 className + container: HTMLElement; // 触发气泡的容器 + unique?: boolean; // 当前气泡展示时,是否关闭其他该配置为 true 的气泡 +} + +export class Popper extends EventEmitter<'show' | 'hide'> { + protected get buttonRect() { + return this.button.getBoundingClientRect(); + } + + protected static conflictPopperList: Popper[] = []; + + // 气泡容器 DOM + public popperDOM!: HTMLElement; + + // 气泡中展示的内容容器 DOM + public contentDOM!: HTMLElement; + /** + * 按钮实体 + * @protected + */ + protected button: HTMLElement; + /** + * Popper 配置 + * @protected + */ + protected option: IPopperOption; + /** + * 当前是否展示 + * @protected + */ + protected isShow: boolean = false; + + /** + * 当前气泡展示的内容 + * @protected + */ + protected content: PopperContent; + + /** + * 关闭气泡的定时器 + * @protected + */ + protected timeout: number | null = null; + + constructor(button: HTMLElement, option: IPopperOption) { + super(); + this.button = button; + this.option = option; + this.init(); + if (option.unique) { + Popper.conflictPopperList.push(this); + } + } + + public getPopperDOM() { + return this.popperDOM; + } + + public getIsShow() { + return this.isShow; + } + + public getContent() { + return this.content; + } + + public setContent(content: PopperContent) { + if (typeof content === 'string') { + this.contentDOM.innerHTML = content; + } else if (content instanceof HTMLElement) { + DOM.clearChildren(this.contentDOM); + this.contentDOM.appendChild(content); + } + this.content = content; + } + + public show = () => { + if (this.isShow || !this.contentDOM.innerHTML) { + return this; + } + this.resetPopperPosition(); + DOM.removeClass(this.popperDOM, 'l7-popper-hide'); + this.isShow = true; + + if (this.option.unique) { + // console.log(Popper.conflictPopperList.length); + Popper.conflictPopperList.forEach((popper) => { + if (popper !== this && popper.isShow) { + popper.hide(); + } + }); + } + this.emit('show'); + return this; + }; + + public hide = () => { + if (!this.isShow) { + return this; + } + DOM.addClass(this.popperDOM, 'l7-popper-hide'); + this.isShow = false; + this.emit('hide'); + return this; + }; + + /** + * 设置隐藏气泡的定时器 + */ + public setHideTimeout = () => { + if (this.timeout) { + return; + } + this.timeout = window.setTimeout(() => { + if (!this.isShow) { + return; + } + this.hide(); + this.timeout = null; + }, 300); + }; + + /** + * 销毁隐藏气泡的定时器 + */ + public clearHideTimeout = () => { + if (this.timeout) { + window.clearTimeout(this.timeout); + this.timeout = null; + } + }; + + public init() { + const { trigger } = this.option; + this.popperDOM = this.createPopper(); + if (trigger === 'click') { + this.button.addEventListener('click', this.onBtnClick); + } else { + this.button.addEventListener('mousemove', this.onBtnMouseMove); + this.button.addEventListener('mouseleave', this.onBtnMouseLeave); + this.popperDOM.addEventListener('mousemove', this.onBtnMouseMove); + this.popperDOM.addEventListener('mouseleave', this.onBtnMouseLeave); + } + } + + public destroy() { + this.button.removeEventListener('click', this.onBtnClick); + this.button.removeEventListener('mousemove', this.onBtnMouseMove); + this.button.removeEventListener('mousemove', this.onBtnMouseLeave); + this.popperDOM.removeEventListener('mousemove', this.onBtnMouseMove); + this.popperDOM.removeEventListener('mouseleave', this.onBtnMouseLeave); + DOM.remove(this.popperDOM); + } + + public resetPopperPosition() { + const popperStyleObj: any = {}; + const { container, offset = [0, 0], placement } = this.option; + const [offsetX, offsetY] = offset; + const buttonRect = this.button.getBoundingClientRect(); + const containerRect = container.getBoundingClientRect(); + const { left, right, top, bottom } = DOM.getDiffRect( + buttonRect, + containerRect, + ); + let isTransformX = false; + let isTransformY = false; + if (/^(left|right)/.test(placement)) { + if (placement.includes('left')) { + popperStyleObj.right = `${buttonRect.width + right}px`; + } else if (placement.includes('right')) { + popperStyleObj.left = `${buttonRect.width + left}px`; + } + if (placement.includes('start')) { + popperStyleObj.top = `${top}px`; + } else if (placement.includes('end')) { + popperStyleObj.bottom = `${bottom}px`; + } else { + popperStyleObj.top = `${top + buttonRect.height / 2}px`; + isTransformY = true; + popperStyleObj.transform = `translate(${offsetX}px, calc(${offsetY}px - 50%))`; + } + } else if (/^(top|bottom)/.test(placement)) { + if (placement.includes('top')) { + popperStyleObj.bottom = `${buttonRect.height + bottom}px`; + } else if (placement.includes('bottom')) { + popperStyleObj.top = `${buttonRect.height + top}px`; + } + if (placement.includes('start')) { + popperStyleObj.left = `${left}px`; + } else if (placement.includes('end')) { + popperStyleObj.right = `${right}px`; + } else { + popperStyleObj.left = `${left + buttonRect.width / 2}px`; + isTransformX = true; + popperStyleObj.transform = `translate(calc(${offsetX}px - 50%), ${offsetY}px)`; + } + } + popperStyleObj.transform = `translate(calc(${offsetX}px - ${ + isTransformX ? '50%' : '0%' + }), calc(${offsetY}px - ${isTransformY ? '50%' : '0%'})`; + + const posList = placement.split('-'); + if (posList.length) { + DOM.addClass( + this.popperDOM, + posList.map((pos) => `l7-popper-${pos}`).join(' '), + ); + } + DOM.addStyle(this.popperDOM, DOM.css2Style(popperStyleObj)); + } + + protected createPopper(): HTMLElement { + const { container, className = '', content } = this.option; + const popper = DOM.create( + 'div', + `l7-popper l7-popper-hide ${className}`, + ) as HTMLElement; + const popperContent = DOM.create('div', 'l7-popper-content') as HTMLElement; + const popperArrow = DOM.create('div', 'l7-popper-arrow') as HTMLElement; + popper.appendChild(popperContent); + popper.appendChild(popperArrow); + container.appendChild(popper); + this.popperDOM = popper; + this.contentDOM = popperContent; + if (content) { + this.setContent(content); + } + return popper; + } + + protected onBtnClick = () => { + if (this.isShow) { + this.hide(); + } else { + this.show(); + } + }; + + protected onBtnMouseLeave = () => { + this.setHideTimeout(); + }; + + protected onBtnMouseMove = () => { + this.clearHideTimeout(); + if (this.isShow) { + return; + } + this.show(); + }; +} diff --git a/packages/component/src/utils/screenfull.ts b/packages/component/src/utils/screenfull.ts new file mode 100644 index 0000000000..87d3fc2071 --- /dev/null +++ b/packages/component/src/utils/screenfull.ts @@ -0,0 +1,159 @@ +// @ts-nocheck + +const methodMap = [ + [ + 'requestFullscreen', + 'exitFullscreen', + 'fullscreenElement', + 'fullscreenEnabled', + 'fullscreenchange', + 'fullscreenerror', + ], + // New WebKit + [ + 'webkitRequestFullscreen', + 'webkitExitFullscreen', + 'webkitFullscreenElement', + 'webkitFullscreenEnabled', + 'webkitfullscreenchange', + 'webkitfullscreenerror', + ], + // Old WebKit + [ + 'webkitRequestFullScreen', + 'webkitCancelFullScreen', + 'webkitCurrentFullScreenElement', + 'webkitCancelFullScreen', + 'webkitfullscreenchange', + 'webkitfullscreenerror', + ], + [ + 'mozRequestFullScreen', + 'mozCancelFullScreen', + 'mozFullScreenElement', + 'mozFullScreenEnabled', + 'mozfullscreenchange', + 'mozfullscreenerror', + ], + [ + 'msRequestFullscreen', + 'msExitFullscreen', + 'msFullscreenElement', + 'msFullscreenEnabled', + 'MSFullscreenChange', + 'MSFullscreenError', + ], +]; + +const nativeAPI = (() => { + if (typeof document === 'undefined') { + return false; + } + + const unprefixedMethods = methodMap[0]; + const returnValue = {}; + + for (const methodList of methodMap) { + const exitFullscreenMethod = methodList?.[1]; + if (exitFullscreenMethod in document) { + for (const [index, method] of methodList.entries()) { + returnValue[unprefixedMethods[index]] = method; + } + + return returnValue; + } + } + + return false; +})(); + +const eventNameMap = { + change: nativeAPI.fullscreenchange, + error: nativeAPI.fullscreenerror, +}; + +let screenfull: any = { + // eslint-disable-next-line default-param-last + request(element = document.documentElement, options) { + return new Promise((resolve, reject) => { + const onFullScreenEntered = () => { + screenfull.off('change', onFullScreenEntered); + resolve(); + }; + + screenfull.on('change', onFullScreenEntered); + + const returnPromise = element[nativeAPI.requestFullscreen](options); + + if (returnPromise instanceof Promise) { + returnPromise.then(onFullScreenEntered).catch(reject); + } + }); + }, + exit() { + return new Promise((resolve, reject) => { + if (!screenfull.isFullscreen) { + resolve(); + return; + } + + const onFullScreenExit = () => { + screenfull.off('change', onFullScreenExit); + resolve(); + }; + + screenfull.on('change', onFullScreenExit); + + const returnPromise = document[nativeAPI.exitFullscreen](); + + if (returnPromise instanceof Promise) { + returnPromise.then(onFullScreenExit).catch(reject); + } + }); + }, + toggle(element, options) { + return screenfull.isFullscreen + ? screenfull.exit() + : screenfull.request(element, options); + }, + onchange(callback) { + screenfull.on('change', callback); + }, + onerror(callback) { + screenfull.on('error', callback); + }, + on(event, callback) { + const eventName = eventNameMap[event]; + if (eventName) { + document.addEventListener(eventName, callback, false); + } + }, + off(event, callback) { + const eventName = eventNameMap[event]; + if (eventName) { + document.removeEventListener(eventName, callback, false); + } + }, + raw: nativeAPI, +}; + +Object.defineProperties(screenfull, { + isFullscreen: { + get: () => Boolean(document[nativeAPI.fullscreenElement]), + }, + element: { + enumerable: true, + get: () => document[nativeAPI.fullscreenElement] ?? undefined, + }, + isEnabled: { + enumerable: true, + // Coerce to boolean in case of old WebKit. + get: () => Boolean(document[nativeAPI.fullscreenEnabled]), + }, +}); + +if (!nativeAPI) { + screenfull = { isEnabled: false }; +} + +export default screenfull; diff --git a/packages/core/src/services/component/ControlService.ts b/packages/core/src/services/component/ControlService.ts index 6a1dfd3d6b..bcf9b32860 100644 --- a/packages/core/src/services/component/ControlService.ts +++ b/packages/core/src/services/component/ControlService.ts @@ -8,7 +8,25 @@ import { IControlCorners, IControlService, IControlServiceCfg, + PositionName, + PositionType, } from './IControlService'; + +const ControlDirectionConfig: Record = { + topleft: 'column', + topright: 'column', + bottomright: 'column', + bottomleft: 'column', + leftcenter: 'column', + rightcenter: 'column', + topcenter: 'row', + bottomcenter: 'row', + lefttop: 'row', + righttop: 'row', + leftbottom: 'row', + rightbottom: 'row', +}; + @injectable() export default class ControlService implements IControlService { public container: HTMLElement; @@ -74,18 +92,21 @@ export default class ControlService implements IControlService { function createCorner(vSideList: string[] = []) { const className = vSideList.map((item) => l + item).join(' '); - corners[vSideList.join('')] = DOM.create('div', className, container); + corners[ + vSideList.filter((item) => !['row', 'column'].includes(item)).join('') + ] = DOM.create('div', className, container); } - createCorner(['top', 'left']); - createCorner(['top', 'right']); - createCorner(['bottom', 'left']); - createCorner(['bottom', 'right']); + function getCornerClassList(positionName: PositionName) { + const positionList = positionName + .replace(/^(top|bottom|left|right|center)/, '$1-') + .split('-'); + return [...positionList, ControlDirectionConfig[positionName]]; + } - createCorner(['top', 'center']); - createCorner(['right', 'center']); - createCorner(['left', 'center']); - createCorner(['bottom', 'center']); + Object.values(PositionType).forEach((position) => { + createCorner(getCornerClassList(position)); + }); } private clearControlPos() { diff --git a/packages/core/src/services/component/IControlService.ts b/packages/core/src/services/component/IControlService.ts index da97702e94..0be75f2238 100644 --- a/packages/core/src/services/component/IControlService.ts +++ b/packages/core/src/services/component/IControlService.ts @@ -1,4 +1,5 @@ import { Container } from 'inversify'; + export enum PositionType { 'TOPRIGHT' = 'topright', 'TOPLEFT' = 'topleft', @@ -8,6 +9,10 @@ export enum PositionType { 'BOTTOMCENTER' = 'bottomcenter', 'LEFTCENTER' = 'leftcenter', 'RIGHTCENTER' = 'rightcenter', + 'LEFTTOP' = 'lefttop', + 'RIGHTTOP' = 'righttop', + 'LEFTBOTTOM' = 'leftbottom', + 'RIGHTBOTTOM' = 'rightbottom', } export type PositionName = @@ -18,21 +23,21 @@ export type PositionName = | 'topcenter' | 'bottomcenter' | 'leftcenter' - | 'rightcenter'; -export interface IControlOption { - name: string; - position: PositionName; - [key: string]: any; -} + | 'rightcenter' + | 'lefttop' + | 'righttop' + | 'leftbottom' + | 'rightbottom'; + export interface IControlServiceCfg { container: HTMLElement; } export interface IControlCorners { [key: string]: HTMLElement; } -export interface IControl { - controlOption: IControlOption; - setPosition(pos: PositionType): void; +export interface IControl { + controlOption: O; + setOptions: (newOption: Partial) => void; addTo(sceneContainer: Container): void; onAdd(): HTMLElement; onRemove(): void; diff --git a/packages/core/src/services/component/IPopupService.ts b/packages/core/src/services/component/IPopupService.ts index c62213983a..621642292d 100644 --- a/packages/core/src/services/component/IPopupService.ts +++ b/packages/core/src/services/component/IPopupService.ts @@ -1,29 +1,112 @@ import { anchorType } from '@antv/l7-utils'; +import EventEmitter from 'eventemitter3'; import { Container } from 'inversify'; import { ILngLat } from '../map/IMapService'; +export type PopupHTML = string | HTMLElement | HTMLElement[]; + export interface IPopupOption { + /** + * 是否展示关闭按钮 + */ closeButton: boolean; + + /** + * 关闭按钮距离右上角的偏移 + */ closeButtonOffsets?: [number, number]; + + /** + * 点击地图区域是否关闭弹框 + */ closeOnClick: boolean; + + /** + * 按 Esc 键是否关闭弹框 + */ + closeOnEsc: boolean; + + /** + * 气泡体的最大宽度 + */ maxWidth: string; + + /** + * 气泡 + */ anchor: anchorType[any]; - className: string; - offsets: number[]; + + /** + * 气泡相对偏移 + */ + offsets: [number, number]; + + /** + * 气泡上的所有鼠标事件是否关闭事件冒泡 + */ stopPropagation: boolean; + + /** + * popup 位置发生变化时地图是否自动平移至气泡位置 + */ + autoPan: boolean; + + /** + * 展示其他气泡时,当前气泡是否自动关闭 + */ + autoClose: boolean; + + /** + * 当前气泡是否自动跟随光标 + */ + followCursor: boolean; + + /** + * 自定义气泡容器的 class + */ + className?: string; + + /** + * 自定义气泡容器的 style + */ + style?: string; + + /** + * Popup 气泡的内置文本 + */ + text?: string; + + /** + * Popup 气泡的内置HTML + */ + html?: PopupHTML; + + /** + * Popup 气泡的标题 + */ + title?: PopupHTML; + + /** + * 初始的经纬度位置 + */ + lngLat?: ILngLat; } -export interface IPopup { + +export interface IPopup extends EventEmitter { addTo(scene: Container): this; remove(): void; setLnglat(lngLat: ILngLat): this; getLnglat(): ILngLat; - setHTML(html: string): this; + setHTML(html: PopupHTML): this; setText(text: string): this; setMaxWidth(maxWidth: string): this; isOpen(): boolean; - open(): void; - close(): void; + open(): this; + close(): this; + getOptions(): IPopupOption; + setOptions(option: Partial): this; } + export interface IPopupService { addPopup(popup: IPopup): void; removePopup(popup: IPopup): void; diff --git a/packages/core/src/services/component/PopupService.ts b/packages/core/src/services/component/PopupService.ts index 47b8f4dd3f..8c2447bf60 100644 --- a/packages/core/src/services/component/PopupService.ts +++ b/packages/core/src/services/component/PopupService.ts @@ -7,33 +7,60 @@ import { IPopup, IPopupService } from './IPopupService'; @injectable() export default class PopupService implements IPopupService { private scene: Container; - private popup: IPopup; private mapsService: IMapService; - private unAddPopup: IPopup | null; + private popups: IPopup[] = []; + private unAddPopups: IPopup[] = []; + + public get isMarkerReady() { + return this.mapsService.map && this.mapsService.getMarkerContainer(); + } public removePopup(popup: IPopup): void { - popup.remove(); + if (popup.isOpen()) { + popup.remove(); + } + + const targetIndex = this.popups.indexOf(popup); + if (targetIndex > -1) { + this.popups.splice(targetIndex, 1); + } + + const targetUnAddIndex = this.unAddPopups.indexOf(popup); + if (targetUnAddIndex > -1) { + this.unAddPopups.splice(targetUnAddIndex, 1); + } } public destroy(): void { - this.popup.remove(); + this.popups.forEach((popup) => popup.remove()); } public addPopup(popup: IPopup) { - if (this.popup) { - this.popup.remove(); + if (popup && popup.getOptions().autoClose) { + [...this.popups, ...this.unAddPopups].forEach((otherPopup) => { + if (otherPopup.getOptions().autoClose) { + this.removePopup(otherPopup); + } + }); } - if (this.mapsService.map && this.mapsService.getMarkerContainer()) { + + if (this.isMarkerReady) { popup.addTo(this.scene); - this.popup = popup; + this.popups.push(popup); } else { - this.unAddPopup = popup; + this.unAddPopups.push(popup); } + + popup.on('close', () => { + this.removePopup(popup); + }); } public initPopup() { - if (this.unAddPopup) { - this.addPopup(this.unAddPopup); - this.unAddPopup = null; + if (this.unAddPopups.length) { + this.unAddPopups.forEach((popup) => { + this.addPopup(popup); + this.unAddPopups = []; + }); } } diff --git a/packages/core/src/services/config/IConfigService.ts b/packages/core/src/services/config/IConfigService.ts index ffbde1d8fa..4925e16378 100644 --- a/packages/core/src/services/config/IConfigService.ts +++ b/packages/core/src/services/config/IConfigService.ts @@ -6,7 +6,7 @@ import { IRenderConfig } from '../renderer/IRendererService'; export interface ISceneConfig extends IRenderConfig { id: string | HTMLDivElement; canvas?: HTMLCanvasElement; - gl?: any, + gl?: any; hasBaseMap?: boolean; map: IMapWrapper; logoPosition?: PositionName; diff --git a/packages/core/src/services/layer/ILayerService.ts b/packages/core/src/services/layer/ILayerService.ts index d640741017..da601f7730 100644 --- a/packages/core/src/services/layer/ILayerService.ts +++ b/packages/core/src/services/layer/ILayerService.ts @@ -270,6 +270,8 @@ export type LayerEventType = | 'mouseenter' | 'unmousemove' | 'mouseout' + | 'show' + | 'hide' | any; export interface ILayer { @@ -605,6 +607,8 @@ export interface ILayerConfig { onClick(pickedFeature: IPickedFeature): void; } +export type LayerServiceEvent = 'layerChange'; + /** * 提供 Layer 管理服务 */ @@ -619,6 +623,9 @@ export interface ILayerService { disableShaderPick: () => void; getShaderPickStat: () => boolean; + on(type: string, handler: (...args: any[]) => void): void; + off(type: string, handler: (...args: any[]) => void): void; + once(type: string, handler: (...args: any[]) => void): void; // 清除画布 clear(): void; add(layer: ILayer): void; diff --git a/packages/core/src/services/layer/LayerService.ts b/packages/core/src/services/layer/LayerService.ts index 1d938ad0c7..39e61cab0c 100644 --- a/packages/core/src/services/layer/LayerService.ts +++ b/packages/core/src/services/layer/LayerService.ts @@ -1,16 +1,17 @@ import { $window, rgb2arr } from '@antv/l7-utils'; +import { EventEmitter } from 'eventemitter3'; import { inject, injectable } from 'inversify'; +import { throttle } from 'lodash'; import 'reflect-metadata'; -import { ILayer } from '../..'; import { TYPES } from '../../types'; import Clock from '../../utils/clock'; import { IMapService } from '../map/IMapService'; import { IRendererService } from '../renderer/IRendererService'; -import { ILayerService } from './ILayerService'; -import { throttle } from 'lodash'; +import { ILayer, ILayerService, LayerServiceEvent } from './ILayerService'; @injectable() -export default class LayerService implements ILayerService { +export default class LayerService extends EventEmitter + implements ILayerService { // pickedLayerId 参数用于指定当前存在被选中的 layer public pickedLayerId: number = -1; public clock = new Clock(); @@ -41,12 +42,12 @@ export default class LayerService implements ILayerService { public reRender = throttle(() => { this.updateLayerRenderList(); this.renderLayers(); - }, 32) + }, 32); public throttleRenderLayers = throttle(() => { this.renderLayers(); - }, 16) - + }, 16); + public add(layer: ILayer) { if (this.sceneInited) { @@ -55,6 +56,7 @@ export default class LayerService implements ILayerService { this.layers.push(layer); this.updateLayerRenderList(); + this.emit('layerChange', this.layers); } public addMask(mask: ILayer) { @@ -118,6 +120,8 @@ export default class LayerService implements ILayerService { } this.updateLayerRenderList(); layer.destroy(); + this.renderLayers(); + this.emit('layerChange', this.layers); } public removeAllLayers() { @@ -187,6 +191,7 @@ export default class LayerService implements ILayerService { this.layers = []; this.layerList = []; this.renderLayers(); + this.emit('layerChange', this.layers); } public startAnimate() { diff --git a/packages/core/src/services/map/IMapService.ts b/packages/core/src/services/map/IMapService.ts index 0a4b4d5364..e7d7ce84d2 100644 --- a/packages/core/src/services/map/IMapService.ts +++ b/packages/core/src/services/map/IMapService.ts @@ -24,7 +24,15 @@ export interface IStatusOptions { zoomEnable: boolean; rotateEnable: boolean; } -export type MapStyle = string | { [key: string]: any }; + +export type MapStyleName = 'normal' | 'light' | 'dark' | string; + +export type MapStyleConfig = { + [key: MapStyleName]: string | any; +}; + +export type MapStyle = MapStyleName | any; + export interface IMapWrapper { setContainer( container: Container, @@ -76,6 +84,10 @@ export interface IMapService { getBounds(): Bounds; getMapContainer(): HTMLElement | null; getMapCanvasContainer(): HTMLElement; + getMapStyleConfig(): MapStyleConfig; // 获取当前地图类型默认的样式配置 + getMapStyleValue(name: MapStyleName): string | any; // 获取当前地图类型key值对应的样式 value,可能为字符串,也可能为对象 + getMapStyle(): MapStyleName | any; // 获取当期地图 + setMapStyle(style: MapStyleName | any): void; // control with raw map setRotation(rotation: number): void; @@ -88,9 +100,8 @@ export interface IMapService { setCenter(center: [number, number], option?: ICameraOptions): void; setPitch(pitch: number): void; setZoom(zoom: number): void; - setMapStyle(style: any): void; setMapStatus(option: Partial): void; - updateView(viewOption:Partial):void // 更新地图视野 + updateView(viewOption: Partial): void; // 更新地图视野 // coordinates methods meterToCoord(center: number[], lnglat: number[]): number; @@ -156,6 +167,7 @@ export interface IEarthService { getBounds(): Bounds; getMapContainer(): HTMLElement | null; getMapCanvasContainer(): HTMLElement; + getMapStyleConfig(): MapStyleConfig; // control with raw map setRotation(rotation: number): void; @@ -268,7 +280,7 @@ export interface IMapConfig { offsetZoom?: number; - interactive: boolean;// + interactive: boolean; // [key: string]: any; } diff --git a/packages/layers/.fatherrc.ts b/packages/layers/.fatherrc.ts index 586f65b4c4..74194fc4de 100644 --- a/packages/layers/.fatherrc.ts +++ b/packages/layers/.fatherrc.ts @@ -1,31 +1,25 @@ export default { // more father 4 config: https://github.com/umijs/father-next/blob/master/docs/config.md esm: { - output:'es' + output: 'es', }, cjs: { - output:'lib' + output: 'lib', }, - platform:'browser', + platform: 'browser', autoprefixer: { browsers: ['IE 11', 'last 2 versions'], }, - extraBabelPresets: [ - '@babel/preset-typescript' - ], + extraBabelPresets: ['@babel/preset-typescript'], extraBabelPlugins: [ // 开发模式下以原始文本引入,便于调试 [ // import glsl as raw text 'babel-plugin-inline-import', { - extensions: [ - '.glsl' - ] - } - ], - [ - 'transform-import-css-l7' + extensions: ['.glsl'], + }, ], + ['transform-import-css-l7'], ], }; diff --git a/packages/layers/src/core/BaseLayer.ts b/packages/layers/src/core/BaseLayer.ts index 9fc690e625..9275f51023 100644 --- a/packages/layers/src/core/BaseLayer.ts +++ b/packages/layers/src/core/BaseLayer.ts @@ -821,6 +821,7 @@ export default class BaseLayer visible: true, }); this.reRender(); + this.emit('show'); return this; } @@ -829,6 +830,7 @@ export default class BaseLayer visible: false, }); this.reRender(); + this.emit('hide'); return this; } public setIndex(index: number): ILayer { diff --git a/packages/map/src/map.ts b/packages/map/src/map.ts index ec81940531..595786f417 100644 --- a/packages/map/src/map.ts +++ b/packages/map/src/map.ts @@ -370,9 +370,8 @@ export class Map extends Camera { } else { width = this.container.clientWidth; height = this.container.clientHeight; - width = width == 0 ? 400 : width; + width = width === 0 ? 400 : width; height = height === 0 ? 300 : height; - } } return [width, height]; diff --git a/packages/maps/src/amap/map.ts b/packages/maps/src/amap/map.ts index d97f91a719..61cced038c 100644 --- a/packages/maps/src/amap/map.ts +++ b/packages/maps/src/amap/map.ts @@ -5,9 +5,9 @@ import AMapLoader from '@amap/amap-jsapi-loader'; import { CoordinateSystem, + IMapCamera, IMapService, IViewport, - IMapCamera, } from '@antv/l7-core'; import { mat4, vec3 } from 'gl-matrix'; import { injectable } from 'inversify'; @@ -15,8 +15,8 @@ import 'reflect-metadata'; import { IAMapEvent, IAMapInstance } from '../../typings/index'; import { Version } from '../version'; -import Viewport from './Viewport'; import AMapBaseService from '../utils/amap/AMapBaseService'; +import Viewport from './Viewport'; // @ts-ignore window.forceWebGL = true; @@ -97,7 +97,7 @@ export default class AMapService extends AMapBaseService id as string | HTMLDivElement, ); const mapConstructorOptions = { - mapStyle: this.getMapStyle(style as string), + mapStyle: this.getMapStyleValue(style as string), zooms: [minZoom, maxZoom], viewMode: '3D', ...rest, @@ -173,6 +173,7 @@ export default class AMapService extends AMapBaseService return coordDis / meterDis; } + // tslint:disable-next-line:no-empty public updateView(viewOption: Partial): void {} public getOverlayContainer(): HTMLElement | undefined { diff --git a/packages/maps/src/amap/theme.ts b/packages/maps/src/amap/theme.ts deleted file mode 100644 index 1a139973d5..0000000000 --- a/packages/maps/src/amap/theme.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const MapTheme: { - [key: string]: any; -} = { - dark: 'amap://styles/c9f1d10cae34f8ab05e425462c5a58d7?isPublic=true', - light: 'amap://styles/c422f5c0cfced5be9fe3a83f05f28a68?isPublic=true', - normal: 'amap://styles/normal', - blank: 'amap://styles/07c17002b38775b32a7a76c66cf90e99?isPublic=true', -}; diff --git a/packages/maps/src/amap2/map.ts b/packages/maps/src/amap2/map.ts index 854a18edab..3ae4cd6b06 100644 --- a/packages/maps/src/amap2/map.ts +++ b/packages/maps/src/amap2/map.ts @@ -6,20 +6,19 @@ import AMapLoader from '@amap/amap-jsapi-loader'; import { Bounds, CoordinateSystem, + IMapCamera, IPoint, IViewport, - IMapCamera, } from '@antv/l7-core'; import { amap2Project, DOM } from '@antv/l7-utils'; import { mat4, vec2, vec3 } from 'gl-matrix'; import { injectable } from 'inversify'; import 'reflect-metadata'; import { IAMapInstance } from '../../typings/index'; +import AMapBaseService from '../utils/amap/AMapBaseService'; import { Version } from '../version'; import './logo.css'; -import { MapTheme } from './theme'; import Viewport from './Viewport'; -import AMapBaseService from '../utils/amap/AMapBaseService'; // @ts-ignore window.forceWebGL = true; @@ -151,16 +150,13 @@ export default class AMapService extends AMapBaseService { } // eslint-disable-next-line @typescript-eslint/no-unused-vars + // tslint:disable-next-line:variable-name no-empty public updateView(_viewOption: Partial): void {} public getOverlayContainer(): HTMLElement | undefined { return undefined; } - protected getMapStyle(name: string): string { - return MapTheme[name] ? MapTheme[name] : name; - } - public getType() { return 'amap2'; } @@ -277,7 +273,7 @@ export default class AMapService extends AMapBaseService { id as string | HTMLDivElement, ); const mapConstructorOptions = { - mapStyle: this.getMapStyle(style as string), + mapStyle: this.getMapStyleValue(style as string), zooms: [minZoom, maxZoom], viewMode: '3D', ...rest, diff --git a/packages/maps/src/amap2/theme.ts b/packages/maps/src/amap2/theme.ts deleted file mode 100644 index 1a139973d5..0000000000 --- a/packages/maps/src/amap2/theme.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const MapTheme: { - [key: string]: any; -} = { - dark: 'amap://styles/c9f1d10cae34f8ab05e425462c5a58d7?isPublic=true', - light: 'amap://styles/c422f5c0cfced5be9fe3a83f05f28a68?isPublic=true', - normal: 'amap://styles/normal', - blank: 'amap://styles/07c17002b38775b32a7a76c66cf90e99?isPublic=true', -}; diff --git a/packages/maps/src/earth/map.ts b/packages/maps/src/earth/map.ts index dd4e15d878..7d22909dba 100644 --- a/packages/maps/src/earth/map.ts +++ b/packages/maps/src/earth/map.ts @@ -93,7 +93,7 @@ export default class L7EarthService extends BaseMapService // @ts-ignore this.map = new EarthMap({ container: this.$mapContainer, - style: this.getMapStyle(style), + style: this.getMapStyleValue(style), bearing: rotation, ...rest, }); diff --git a/packages/maps/src/map/map.ts b/packages/maps/src/map/map.ts index aa0b477256..f0b943702b 100644 --- a/packages/maps/src/map/map.ts +++ b/packages/maps/src/map/map.ts @@ -59,7 +59,7 @@ export default class L7MapService extends BaseMapService { // @ts-ignore this.map = new Map({ container: this.$mapContainer, - style: this.getMapStyle(style), + style: this.getMapStyleValue(style), bearing: rotation, ...rest, }); @@ -93,7 +93,7 @@ export default class L7MapService extends BaseMapService { this.map = new Map({ container: this.$mapContainer as HTMLElement, - style: this.getMapStyle(style), + style: this.getMapStyleValue(style), bearing: rotation, // @ts-ignore canvas, diff --git a/packages/maps/src/mapbox/map.ts b/packages/maps/src/mapbox/map.ts index 735f6e36b7..a98fb8e1ec 100644 --- a/packages/maps/src/mapbox/map.ts +++ b/packages/maps/src/mapbox/map.ts @@ -144,7 +144,7 @@ export default class MapboxService extends BaseMapService< // @ts-ignore this.map = new window.mapboxgl.Map({ container: this.$mapContainer, - style: this.getMapStyle(style), + style: this.getMapStyleValue(style), attributionControl, bearing: rotation, ...rest, diff --git a/packages/maps/src/utils/BaseMapService.ts b/packages/maps/src/utils/BaseMapService.ts index 021f37bc18..03343301c8 100644 --- a/packages/maps/src/utils/BaseMapService.ts +++ b/packages/maps/src/utils/BaseMapService.ts @@ -15,15 +15,17 @@ import { IStatusOptions, IViewport, MapServiceEvent, - MapStyle, + MapStyleConfig, + MapStyleName, TYPES, } from '@antv/l7-core'; import { Map } from '@antv/l7-map'; import { DOM } from '@antv/l7-utils'; import { inject, injectable } from 'inversify'; import 'reflect-metadata'; -import { ISimpleMapCoord, SimpleMapCoord } from './simpleMapCoord'; import { Version } from '../version'; +import { ISimpleMapCoord, SimpleMapCoord } from './simpleMapCoord'; +import { MapTheme } from './theme'; const EventMap: { [key: string]: any; } = { @@ -32,7 +34,6 @@ const EventMap: { zoomchange: 'zoom', dragging: 'drag', }; -import { MapTheme } from './theme'; const LNGLAT_OFFSET_ZOOM_THRESHOLD = 12; /** @@ -43,10 +44,10 @@ export default abstract class BaseMapService implements IMapService { public version: string = Version.L7MAP; public map: Map & T; - protected viewport: IViewport | unknown; public simpleMapCoord: ISimpleMapCoord = new SimpleMapCoord(); // 背景色 public bgColor: string = 'rgba(0.0, 0.0, 0.0, 0.0)'; + protected viewport: IViewport | unknown; @inject(TYPES.MapConfig) protected readonly config: Partial; @@ -227,7 +228,7 @@ export default abstract class BaseMapService } public setMapStyle(style: any): void { - this.map.setStyle(this.getMapStyle(style)); + this.map.setStyle(this.getMapStyleValue(style)); } // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -264,6 +265,28 @@ export default abstract class BaseMapService origin: IMercator, ): number[]; + public getMapStyle(): string { + try { + // @ts-ignore + const styleUrl = this.map.getStyle().sprite ?? ''; + // 将 Mapbox 返回的样式字符串转成传入 style 保持一致 + if (/^mapbox:\/\/sprites\/zcxduo\/\w+\/\w+$/.test(styleUrl)) { + return styleUrl?.replace(/\/\w+$/, '').replace(/sprites/, 'styles'); + } + return styleUrl; + } catch (e) { + return ''; + } + } + + public getMapStyleConfig(): MapStyleConfig { + return MapTheme; + } + + public getMapStyleValue(name: MapStyleName): any { + return this.getMapStyleConfig()[name] ?? name; + } + public abstract init(): Promise; public destroy() { @@ -356,11 +379,4 @@ export default abstract class BaseMapService this.coordinateSystemService.setCoordinateSystem(CoordinateSystem.LNGLAT); } } - - protected getMapStyle(name: MapStyle) { - if (typeof name !== 'string') { - return name; - } - return MapTheme[name] ? MapTheme[name] : name; - } } diff --git a/packages/maps/src/utils/amap/AMapBaseService.ts b/packages/maps/src/utils/amap/AMapBaseService.ts index 273a7e681e..66b4bd3100 100644 --- a/packages/maps/src/utils/amap/AMapBaseService.ts +++ b/packages/maps/src/utils/amap/AMapBaseService.ts @@ -9,6 +9,7 @@ import { ICoordinateSystemService, IGlobalConfigService, ILngLat, + IMapCamera, IMapConfig, IMapService, IMercator, @@ -16,20 +17,22 @@ import { IStatusOptions, IViewport, MapServiceEvent, + MapStyleConfig, + MapStyleName, TYPES, - IMapCamera, } from '@antv/l7-core'; import { DOM } from '@antv/l7-utils'; import { mat4, vec3 } from 'gl-matrix'; import { inject, injectable } from 'inversify'; import 'reflect-metadata'; import { IAMapEvent, IAMapInstance } from '../../../typings/index'; +import { Version } from '../../version'; import { ISimpleMapCoord, SimpleMapCoord } from '../simpleMapCoord'; import { toPaddingOptions } from '../utils'; -import { Version } from '../../version'; import Viewport from '../Viewport'; import './logo.css'; import { MapTheme } from './theme'; + let mapdivCount = 0; // @ts-ignore window.forceWebGL = true; @@ -54,7 +57,7 @@ const LNGLAT_OFFSET_ZOOM_THRESHOLD = 12; // 暂时关闭 fix 统一不同坐标 * AMapService */ @injectable() -export default class AMapBaseService +export default abstract class AMapBaseService implements IMapService { public version: string = Version['GAODE1.x']; public simpleMapCoord: ISimpleMapCoord = new SimpleMapCoord(); @@ -84,6 +87,7 @@ export default class AMapBaseService protected viewport: IViewport; protected cameraChangedCallback: (viewport: IViewport) => void; + public setBgColor(color: string) { this.bgColor = color; } @@ -252,13 +256,26 @@ export default class AMapBaseService this.map.setZoomAndCenter(zoom + 1, center); } - public setMapStyle(style: string): void { - this.map.setMapStyle(this.getMapStyle(style)); + public setMapStyle(style: MapStyleName): void { + this.map.setMapStyle(this.getMapStyleValue(style)); } public setMapStatus(option: Partial): void { this.map.setStatus(option); } + + public getMapStyleConfig(): MapStyleConfig { + return MapTheme; + } + + public getMapStyleValue(name: MapStyleName) { + return this.getMapStyleConfig()[name] || name; + } + + public getMapStyle(): string { + return this.map.getMapStyle(); + } + public pixelToLngLat(pixel: [number, number]): ILngLat { const lngLat = this.map.pixelToLngLat(new AMap.Pixel(pixel[0], pixel[1])); return { lng: lngLat.getLng(), lat: lngLat.getLat() }; @@ -311,7 +328,6 @@ export default class AMapBaseService altitude: number, rotate: [number, number, number], scale: [number, number, number] = [1, 1, 1], - ): number[] { const flat = this.viewport.projectFlat(lnglat); // @ts-ignore @@ -362,7 +378,7 @@ export default class AMapBaseService id as string | HTMLDivElement, ); const mapConstructorOptions = { - mapStyle: this.getMapStyle(style as string), + mapStyle: this.getMapStyleValue(style as string), zooms: [minZoom, maxZoom], viewMode: '3D', ...rest, @@ -440,6 +456,7 @@ export default class AMapBaseService // eslint-disable-next-line @typescript-eslint/no-unused-vars public updateView(viewOption: Partial): void {} + public getOverlayContainer(): HTMLElement | undefined { return undefined; } @@ -538,10 +555,6 @@ export default class AMapBaseService this.cameraChangedCallback(this.viewport); } }; - - protected getMapStyle(name: string): string { - return MapTheme[name] ? MapTheme[name] : name; - } protected creatMapContainer(id: string | HTMLDivElement) { let $wrapper = id as HTMLDivElement; if (typeof id === 'string') { diff --git a/packages/maps/src/utils/amap/theme.ts b/packages/maps/src/utils/amap/theme.ts index 1a139973d5..61aef99884 100644 --- a/packages/maps/src/utils/amap/theme.ts +++ b/packages/maps/src/utils/amap/theme.ts @@ -1,8 +1,14 @@ -export const MapTheme: { - [key: string]: any; -} = { - dark: 'amap://styles/c9f1d10cae34f8ab05e425462c5a58d7?isPublic=true', - light: 'amap://styles/c422f5c0cfced5be9fe3a83f05f28a68?isPublic=true', +import { MapStyleConfig } from '@antv/l7-core'; + +export const MapTheme: MapStyleConfig = { normal: 'amap://styles/normal', + light: 'amap://styles/c422f5c0cfced5be9fe3a83f05f28a68?isPublic=true', + dark: 'amap://styles/c9f1d10cae34f8ab05e425462c5a58d7?isPublic=true', blank: 'amap://styles/07c17002b38775b32a7a76c66cf90e99?isPublic=true', + fresh: 'amap://styles/fresh', + grey: 'amap://styles/grey', + graffiti: 'amap://styles/graffiti', + macaron: 'amap://styles/macaron', + darkblue: 'amap://styles/darkblue', + wine: 'amap://styles/wine', }; diff --git a/packages/scene/package.json b/packages/scene/package.json index 50a1af3652..016521a2fc 100644 --- a/packages/scene/package.json +++ b/packages/scene/package.json @@ -30,6 +30,7 @@ "@antv/l7-renderer": "2.9.37", "@antv/l7-utils": "2.9.37", "@babel/runtime": "^7.7.7", + "eventemitter3": "^4.0.7", "inversify": "^5.0.1", "mapbox-gl": "^1.2.1", "reflect-metadata": "^0.1.13" diff --git a/packages/scene/src/boxSelect.ts b/packages/scene/src/boxSelect.ts new file mode 100644 index 0000000000..afced935b4 --- /dev/null +++ b/packages/scene/src/boxSelect.ts @@ -0,0 +1,123 @@ +import { DOM, getBBoxFromPoints } from '@antv/l7-utils'; +import { EventEmitter } from 'eventemitter3'; +import { Scene } from './index'; + +export const BoxSelectEventList = ['selectstart', 'selecting', 'selectend']; + +export type BoxSelectOptions = { + className?: string; +}; + +// TODO: 将 BoxSelect 模块放在哪里比较合适 +export default class BoxSelect extends EventEmitter { + protected scene: Scene; + protected options: BoxSelectOptions; + + protected isEnable = false; + protected box: HTMLElement; + protected startEvent: any; + protected endEvent: any; + + constructor(scene: Scene, options: BoxSelectOptions = {}) { + super(); + + this.scene = scene; + this.options = options; + } + + public get container() { + return this.scene.getMapService().getMarkerContainer(); + } + + public enable() { + if (this.isEnable) { + return; + } + const { className } = this.options; + this.scene.setMapStatus({ + dragEnable: false, + }); + this.container.style.cursor = 'crosshair'; + if (!this.box) { + const box = DOM.create( + 'div', + undefined, + this.container, + ) as HTMLDivElement; + box.classList.add('l7-select-box'); + if (className) { + box.classList.add(className); + } + box.style.display = 'none'; + this.box = box; + } + this.scene.on('dragstart', this.onDragStart); + this.scene.on('dragging', this.onDragging); + this.scene.on('dragend', this.onDragEnd); + this.isEnable = true; + } + + public disable() { + if (!this.isEnable) { + return; + } + this.scene.setMapStatus({ + dragEnable: true, + }); + this.container.style.cursor = 'auto'; + this.scene.off('dragstart', this.onDragStart); + this.scene.off('dragging', this.onDragging); + this.scene.off('dragend', this.onDragEnd); + this.isEnable = false; + } + + protected onDragStart = (e: any) => { + this.box.style.display = 'block'; + this.startEvent = this.endEvent = e; + this.syncBoxBound(); + this.emit( + 'selectstart', + this.getLngLatBox(), + this.startEvent, + this.endEvent, + ); + }; + + protected onDragging = (e: any) => { + this.endEvent = e; + this.syncBoxBound(); + this.emit('selecting', this.getLngLatBox(), this.startEvent, this.endEvent); + }; + + protected onDragEnd = (e: any) => { + this.endEvent = e; + this.box.style.display = 'none'; + this.emit('selectend', this.getLngLatBox(), this.startEvent, this.endEvent); + }; + + protected syncBoxBound() { + const { x: x1, y: y1 } = this.startEvent; + const { x: x2, y: y2 } = this.endEvent; + const left = Math.min(x1, x2); + const top = Math.min(y1, y2); + const width = Math.abs(x1 - x2); + const height = Math.abs(y1 - y2); + this.box.style.top = `${top}px`; + this.box.style.left = `${left}px`; + this.box.style.width = `${width}px`; + this.box.style.height = `${height}px`; + } + + protected getLngLatBox() { + const { + lngLat: { lng: lng1, lat: lat1 }, + } = this.startEvent; + const { + lngLat: { lng: lng2, lat: lat2 }, + } = this.endEvent; + return getBBoxFromPoints([ + [lng1, lat1], + [lng2, lat2], + ]); + } +} diff --git a/packages/scene/src/index.ts b/packages/scene/src/index.ts index 74f802fe34..2dbd0139a3 100644 --- a/packages/scene/src/index.ts +++ b/packages/scene/src/index.ts @@ -34,6 +34,7 @@ import { MaskLayer } from '@antv/l7-layers'; import { ReglRendererService } from '@antv/l7-renderer'; import { DOM, isMini } from '@antv/l7-utils'; import { Container } from 'inversify'; +import BoxSelect, { BoxSelectEventList } from './boxSelect'; import ILayerManager from './ILayerManager'; import IMapController from './IMapController'; import IPostProcessingPassPluggable from './IPostProcessingPassPluggable'; @@ -61,10 +62,11 @@ class Scene private popupService: IPopupService; private fontService: IFontService; private interactionService: IInteractionService; + private boxSelect: BoxSelect; private container: Container; public constructor(config: ISceneConfig) { - const { id, map, canvas, hasBaseMap, } = config; + const { id, map, canvas, hasBaseMap } = config; // 创建场景容器 const sceneContainer = createSceneContainer(); this.container = sceneContainer; @@ -96,6 +98,7 @@ class Scene TYPES.IInteractionService, ); this.popupService = sceneContainer.get(TYPES.IPopupService); + this.boxSelect = new BoxSelect(this, {}); if (isMini) { this.sceneService.initMiniScene(config); @@ -258,7 +261,6 @@ class Scene * @param name */ public addIconFont(name: string, fontUnicode: string): void { - this.fontService.addIconFont(name, fontUnicode); } @@ -273,8 +275,8 @@ class Scene * @param fontPath */ public addFontFace(fontFamily: string, fontPath: string): void { - this.fontService.once('fontloaded', (e)=>{ - this.emit('fontloaded',e) + this.fontService.once('fontloaded', (e) => { + this.emit('fontloaded', e); }); this.fontService.addFontFace(fontFamily, fontPath); } @@ -333,16 +335,28 @@ class Scene this.popupService.addPopup(popup); } + public removePopup(popup: IPopup) { + this.popupService.removePopup(popup); + } + public on(type: string, handle: (...args: any[]) => void): void { - SceneEventList.indexOf(type) === -1 - ? this.mapService.on(type, handle) - : this.sceneService.on(type, handle); + if (BoxSelectEventList.includes(type)) { + this.boxSelect?.on(type, handle); + } else if (SceneEventList.includes(type)) { + this.sceneService.on(type, handle); + } else { + this.mapService.on(type, handle); + } } public once(type: string, handle: (...args: any[]) => void): void { - SceneEventList.indexOf(type) === -1 - ? this.mapService.once(type, handle) - : this.sceneService.once(type, handle); + if (BoxSelectEventList.includes(type)) { + this.boxSelect?.once(type, handle); + } else if (SceneEventList.includes(type)) { + this.sceneService.once(type, handle); + } else { + this.mapService.once(type, handle); + } } public emit(type: string, handle: (...args: any[]) => void): void { SceneEventList.indexOf(type) === -1 @@ -350,11 +364,14 @@ class Scene : this.sceneService.emit(type, handle); } - public off(type: string, handle: (...args: any[]) => void): void { - SceneEventList.indexOf(type) === -1 - ? this.mapService.off(type, handle) - : this.sceneService.off(type, handle); + if (BoxSelectEventList.includes(type)) { + this.boxSelect?.off(type, handle); + } else if (SceneEventList.includes(type)) { + this.sceneService.off(type, handle); + } else { + this.mapService.off(type, handle); + } } // implements IMapController @@ -477,6 +494,19 @@ class Scene this.layerService.disableShaderPick(); } + public enableBoxSelect(once = true) { + this.boxSelect.enable(); + if (once) { + this.boxSelect.once('selectend', () => { + this.disableBoxSelect(); + }); + } + } + + public disableBoxSelect() { + this.boxSelect.disable(); + } + // get current point size info public getPointSizeRange() { return this.sceneService.getPointSizeRange(); diff --git a/packages/site/docs/api/component/component.zh.md b/packages/site/docs/api/component/component.zh.md new file mode 100644 index 0000000000..a137e58fed --- /dev/null +++ b/packages/site/docs/api/component/component.zh.md @@ -0,0 +1,10 @@ +--- +title: Component 组件 +order: 0 +--- + +L7 中的 Component 主要包含以下三种类型: + +- [Control 控件类型](./control/control):指的是悬停在地图四周,可以对地图以及图层等元素进行信息呈现或交互的组件。 +- [Popup 气泡类型](./popup/popup):用于在地图上指定经纬度位置展示气泡,气泡内容完全交由开发者自定义。 +- [Marker 类型](./marker/marker):与 Popup 相似,不同的是 Marker 展示的内容不会在气泡内展示,而是完全交由开发者自定义。 diff --git a/packages/site/docs/api/component/control.en.md b/packages/site/docs/api/component/control.en.md deleted file mode 100644 index 2c4735a812..0000000000 --- a/packages/site/docs/api/component/control.en.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -title: Control -order: 3 ---- - -`markdown:docs/common/style.md` - -地图组件 用于控制地图的状态如果平移,缩放,或者展示地图一些的辅助信息如图例,比例尺 - -L7 目前支持 Control - -- Zoom 放大缩小 -- Scale 比例尺 -- Layers 图层列表 - -## 构造函数 - -### option - -position: `string` 控件位置支持是个方位 - -- bottomright -- topright -- bottomleft -- topleft -- topcenter -- bottomcenter -- leftcenter -- rightcenter - -### 组件介绍 - -``` -import { Scale, Layers, Zoom } from '@antv/l7'; - -``` - -#### Zoom - -放大缩小组件 默认左上角 - -```javascript -const zoomControl = new Zoom({ - position: 'topleft', -}); - -scene.addControl(zoomControl); -``` - -#### Scale - -比例尺组件 默认左下角 - -```javascript -const scaleControl = new Scale({ - position: 'bottomleft', -}); - -scene.addControl(scaleControl); -``` - -#### Layers - -图层列表目前支持可视化的图层控制 - -```javascript -const overlayers = { - 点图层: layer, -}; -const layersControl = new Layers({ - overlayers, -}); -scene.addControl(layersControl); -``` - -## 方法 - -#### setPosition - -设置组件位置 - -```javascript -control.setPosition('bottomright'); -``` - -#### remove - -移除地图组件 - -```javascript -control.remove(); -``` diff --git a/packages/site/docs/api/component/control.zh.md b/packages/site/docs/api/component/control.zh.md deleted file mode 100644 index b380b3a851..0000000000 --- a/packages/site/docs/api/component/control.zh.md +++ /dev/null @@ -1,108 +0,0 @@ ---- -title: Control 控件 -order: 3 ---- - -`markdown:docs/common/style.md` - -地图组件用于控制地图的状态如果平移,缩放,或者展示地图一些的辅助信息如图例、比例尺 - -L7 目前支持 Control - -- Zoom 放大缩小 -- Scale 比例尺 -- Layers 图层列表 - -## 构造函数 - -#### option - -position: `string` 控件位置支持 8 个方位 - -- bottomright -- topright -- bottomleft -- topleft -- topcenter -- bottomcenter -- leftcenter -- rightcenter - -### 组件介绍 - -``` -import { Scale, Layers, Zoom } from '@antv/l7'; - -``` - -#### Zoom - -放大缩小组件 默认左上角 - -```javascript -const zoomControl = new Zoom({ - position: 'topleft', -}); - -scene.addControl(zoomControl); -``` - -#### Scale - -比例尺组件 默认左下角 - -```javascript -const scaleControl = new Scale({ - position: 'bottomleft', -}); - -scene.addControl(scaleControl); -``` - -#### Layers - -图层列表目前支持可视化的图层控制 - -配置项 - -option 控件配置项 -overlayers 将一组图层添加到图层列表, -overlayers Object -key: 列表显示的图层名字可以自定义 -layer: 图层对象 -overlayers 示例 - -```javascript -const layer = { - 图层一: layer1, - 图层二: layer2, -}; -``` - -```javascript -const overlayers = { - 点图层: layer, -}; -const layersControl = new Layers({ - overlayers, -}); -scene.addControl(layersControl); -``` - -## 方法 - -#### setPosition - -设置组件位置 - -```javascript -control.setPosition('bottomright'); -``` - -#### remove - -移除地图组件 - -```javascript -control.remove(); -``` diff --git a/packages/site/docs/api/component/control/control.zh.md b/packages/site/docs/api/component/control/control.zh.md new file mode 100644 index 0000000000..0a615446c4 --- /dev/null +++ b/packages/site/docs/api/component/control/control.zh.md @@ -0,0 +1,66 @@ +--- +title: 控件 +order: 1 +--- + +地图控件指的是悬停在地图四周,可以对地图以及图层等元素进行**信息呈现**或**交互**的组件。 + +![](https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*zgFeTocc-_oAAAAAAAAAAAAAARQnAQ) + +## 使用 + +```ts +import { Scene, Zoom } from '@antv/l7'; + +const scene = new Scene({ + // ... +}); + +scene.on('loaded', () => { + // 实例化 Zoom 控件,可以在构造器中传入控件的配置 + const zoom = new Zoom({ + position: 'leftbottom', + className: 'my-test-class', + }); + + // 将实例化的控件添加至 L7 中 + scene.addControl(zoom); +}); +``` + +## 更新配置 + +在控件实例化之后,如果需要更新配置,可以调用控件实例的 `setOptions` 方法,同时传入需要更新的配置对象即可。 + +```ts +const zoom = new Zoom({ + position: 'leftbottom', +}); + +const onPositionChange = () => { + // 通过 setOptions 传入需要更新的配置对象 + zoom.setOptions({ + position: 'topright', + }); +}; +``` + +## 插槽 + +当前 L7 中的控件支持插入到地图的**左上、左下、右上、右下、上、左、下、右**八个位置的控件插槽中,并且在同一插槽中的多个控件支持**横向**和**纵向**排列。 + +在初始化所有的控件类时,可以传入 `position` 参数来设置控件对应的插槽以及排列方式。 + +![](https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*BfG1TI231ysAAAAAAAAAAAAAARQnAQ) + +## 配置 + +`markdown:docs/common/control/api.md` + +## 方法 + +`markdown:docs/common/control/method.md` + +## 事件 + +`markdown:docs/common/control/event.md` diff --git a/packages/site/docs/api/component/control/exportImage.zh.md b/packages/site/docs/api/component/control/exportImage.zh.md new file mode 100644 index 0000000000..8986e9f018 --- /dev/null +++ b/packages/site/docs/api/component/control/exportImage.zh.md @@ -0,0 +1,81 @@ +--- +title: ExportImage 导出图片 +order: 7 +--- + +对当前地图部分进行截图并生成图片的 `Base64` 字符串。 + + + +## 说明 + +[示例](/zh/examples/component/control#exportimage) + +截图时被截取的目标仅包含: + +- 地图底图 +- 图层(不包含 MarkerLayer) + +**注意:由于当前地图底图对应 `Canvas` 默认开启了缓冲区,导致默认情况下无法截取到地图底图部分。** + +因此若开发者需要完整的截图能力,则应当在初始化地图实例时传递以下参数以关闭 `Canvas` 缓冲区。 + +```ts +new GaodeMapV2({ + WebGLParams: { + preserveDrawingBuffer: true, + }, +}); + +new Mapbox({ + preserveDrawingBuffer: true, +}); +``` + +## 使用 + +```ts +import { Scene, ExportImage } from '@antv/l7'; + +const scene = new Scene({ + id: 'map', + map: new GaodeMapV2({ + // 关闭地图缓冲区,否则截图时无法截取到地图部分 + WebGLParams: { + preserveDrawingBuffer: true, + }, + }), +}); + +scene.on('loaded', () => { + const zoom = new ExportImage({ + onExport: (base64: string) => { + // download(base64) + }, + }); + scene.addControl(zoom); +}); +``` + +## 配置 + +| 名称 | 说明 | 类型 | +| --------- | -------------------------------------------------- | -------------------------- | +| imageType | 截图图片的格式 | `'png'` | `'jpeg'` | +| onExport | 截图成功后,用于接收图片 `Base64` 字符串的回调函数 | `(base64: string) => void` | + +`markdown:docs/common/control/btn-api.md` + +`markdown:docs/common/control/api.md` + +## 方法 + +| 名称 | 说明 | 类型 | +| -------- | ------------------------ | ----------------------- | +| getImage | 获取截图的 Base64 字符串 | `() => Promise` | + +`markdown:docs/common/control/method.md` + +## 事件 + +`markdown:docs/common/control/event.md` diff --git a/packages/site/docs/api/component/control/fullscreen.zh.md b/packages/site/docs/api/component/control/fullscreen.zh.md new file mode 100644 index 0000000000..a62e189b0f --- /dev/null +++ b/packages/site/docs/api/component/control/fullscreen.zh.md @@ -0,0 +1,56 @@ +--- +title: Fullscreen 全屏 +order: 5 +--- + +用于控制地图区域的**全屏**和**退出全屏**的控制按钮控件。 + + + +## 使用 + +[示例](/zh/examples/component/control#fullscreen) + +```ts +import { Scene, Fullscreen } from '@antv/l7'; + +const scene = new Scene({ + // ... +}); + +scene.on('loaded', () => { + const fullscreen = new Fullscreen({ + btnText: '全屏', + exitBtnText: '退出全屏', + }); + scene.addControl(fullscreen); +}); +``` + +## 配置 + +`markdown:docs/common/control/btn-api.md` + +| 名称 | 说明 | 类型 | +| ----------- | --------------------------------- | --------------------------------- | +| exitBtnIcon | 退出全屏按钮的图标 | `HTMLElement` | `SVGElement` | +| exitBtnText | 退出全屏按钮的文本 | `string` | +| exitTitle | 退出全屏按钮的文本的 `title` 属性 | `string` | + +`markdown:docs/common/control/api.md` + +## 方法 + +| 名称 | 说明 | 类型 | +| ---------------- | ------------------------------- | ------------ | +| toggleFullscreen | 进入/退出全屏地图部分的全屏状态 | `() => void` | + +`markdown:docs/common/control/method.md` + +## 事件 + +| 名称 | 说明 | 类型 | +| ---------------- | ------------------------ | --------------------------------- | +| fullscreenChange | 当全屏状态发生变化时触发 | `(isFullscreen: boolean) => void` | + +`markdown:docs/common/control/event.md` diff --git a/packages/site/docs/api/component/control/geolocate.zh.md b/packages/site/docs/api/component/control/geolocate.zh.md new file mode 100644 index 0000000000..c9949f09e6 --- /dev/null +++ b/packages/site/docs/api/component/control/geolocate.zh.md @@ -0,0 +1,60 @@ +--- +title: GeoLocate 定位 +order: 6 +--- + +使用浏览器环境的 `nagigator` 的 `getlocation` 方法,使用浏览器打开位置感应能力获取当前用户所在经纬度。 + + + +## 说明 + +[示例](/zh/examples/component/control#geolocate) + +**注意:** + +- 在使用该能力时,会需要用户对浏览器打开位置感知能力进行鉴权。 +- 当前浏览器获取到的坐标是 `WGS84` 地理坐标系,在高德地图上使用会有偏差,可以使用 `transform` 配置进行坐标系的转换。 + +## 使用 + +```ts +import { Scene, GeoLocate } from '@antv/l7'; +import gcoord from 'gcoord'; + +const scene = new Scene({ + // ... +}); + +scene.on('loaded', () => { + const geoLocate = new GeoLocate({ + transform: (position) => { + // 将获取到基于 WGS84 地理坐标系 的坐标转成 GCJ02 坐标系 + return gcoord.transform(position, gcoord.WGS84, gcoord.GCJ02); + }, + }); + scene.addControl(geoLocate); +}); +``` + +## 配置 + +| 名称 | 说明 | 类型 | +| --------- | ----------------------------------------------------------------------- | -------------------------------------------------- | +| transform | 格式化通过 `getlocation` 获取到的经纬度的函数,可以用于地理坐标系的转换 | `(position: [number, number]) => [number, number]` | + +`markdown:docs/common/control/btn-api.md` + +`markdown:docs/common/control/api.md` + +## 方法 + +| 名称 | 说明 | 类型 | +| -------------- | ---------------------- |-----------------------------------| +| getGeoLocation | 获取当前用户所在经纬度 | `() => Promise<[number, number]>` | + +`markdown:docs/common/control/method.md` + +## 事件 + +`markdown:docs/common/control/event.md` diff --git a/packages/site/docs/api/component/control/layerControl.zh.md b/packages/site/docs/api/component/control/layerControl.zh.md new file mode 100644 index 0000000000..1898d0f0c2 --- /dev/null +++ b/packages/site/docs/api/component/control/layerControl.zh.md @@ -0,0 +1,60 @@ +--- +title: LayerControl 图层显隐 +order: 8 +--- + +用于控制目标图层组的**显示**和**隐藏**操作。 + + + +## 说明 + +**注意**: 在控件中展示的图层名称会默认读取图层的 `name` 属性,因此需要用户在初始化图层时传入图层对应的名称。 + +## 使用 + +[示例](/zh/examples/component/control#layercontrol) + +```ts +import { Scene, LayerControl } from '@antv/l7'; + +const scene = new Scene({ + // ... +}); + +scene.on('loaded', () => { + const layer = new PointLayer({ + name: '自定义图层名称', + }); + scene.addLayer(layer); + + const layerControl = new LayerControl({ + layers: [layer], + }); + scene.addControl(layerControl); +}); +``` + +## 配置 + +| 名称 | 说明 | 类型 | +| ------ | ------------------------------------------------------------- | --------------- | +| layers | 需要被控制的 `layer` 数组,不传则默认读取当前 L7 中所有的图层 | `Array` | + +`markdown:docs/common/control/popper-api.md` + +`markdown:docs/common/control/btn-api.md` + +`markdown:docs/common/control/api.md` + +## 方法 + +`markdown:docs/common/control/method.md` + +## 事件 + +`markdown:docs/common/control/event.md` + +`markdown:docs/common/control/popper-event.md` + +`markdown:docs/common/control/select-event.md` diff --git a/packages/site/docs/api/component/control/logo.zh.md b/packages/site/docs/api/component/control/logo.zh.md new file mode 100644 index 0000000000..ded002d35e --- /dev/null +++ b/packages/site/docs/api/component/control/logo.zh.md @@ -0,0 +1,64 @@ +--- +title: Logo 标志 +order: 2 +--- + +用于在地图上展示 Logo 图片的控件,并且支持超链接点击跳转。 + +![](https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*CbdSRLizMLIAAAAAAAAAAAAAARQnAQ) + +## 说明 + +当前 L7 会默认在地图左下角展示该控件,如需隐藏可以在 Scene 实例化时配置: + +```ts +import { Scene } from '@antv/l7'; + +const scene = new Scene({ + // ... + + // 关闭默认 L7 Logo + logoVisible: false, +}); +``` + +## 使用 + +[示例](/zh/examples/component/control#logo) + +```ts +import { Scene, Logo } from '@antv/l7'; + +const scene = new Scene({ + //... + logoVisible: false, +}); + +scene.on('loaded', () => { + const logo = new Logo({ + // 图片 url + img: + 'https://gw.alipayobjects.com/mdn/rms_816329/afts/img/A*GRb1TKp4HcMAAAAAAAAAAAAAARQnAQ', + // 跳转地址 + href: 'https://l7.antv.vision/', + }); + scene.addControl(logo); +}); +``` + +## 配置 + +| 名称 | 说明 | 类型 | +| ---- | ---------------------------------------------------- | -------- | +| img | Logo 展示的图片 url | `string` | +| href | 点击 Logo 跳转的超链接,不传则纯展示图片,点击不跳转 | `string` | + +`markdown:docs/common/control/api.md` + +## 方法 + +`markdown:docs/common/control/method.md` + +## 事件 + +`markdown:docs/common/control/event.md` diff --git a/packages/site/docs/api/component/control/mapTheme.zh.md b/packages/site/docs/api/component/control/mapTheme.zh.md new file mode 100644 index 0000000000..8c92be361d --- /dev/null +++ b/packages/site/docs/api/component/control/mapTheme.zh.md @@ -0,0 +1,66 @@ +--- +title: MapTheme 地图主题 +order: 11 +--- + +该控件用于切换地图底图的主题样式。 + + + +## 说明 + +MapTheme 会根据当前地图底图类型(如 Mapbox、GaodeMapV2),默认展示相对应的默认主题选项,用于也可以传入自定义的主题选项 + +## 使用 + +[示例](/zh/examples/component/control#maptheme) + +```ts +import { Scene, MapTheme } from '@antv/l7'; + +const scene = new Scene({ + // ... +}); + +scene.on('loaded', () => { + const mapTheme = new MapTheme({}); + scene.addControl(mapTheme); +}); +``` + +## 配置 + +| 名称 | 说明 | 类型 | +| ------- | -------------------------------------------------------------------------------------- | --------------------------- | +| options | 用户自定义的地图主题选项,每个选项的类型可见 [IControlOptionItem](#icontroloptionitem) | `Array` | + +## IControlOptionItem + +```ts +export type IControlOptionItem = { + // 主题选项对应的文本 + text: string; + // 主题选项对应地图主题 style 的 key 值 + value: string; + // 主题选项对应展示的图片 + img?: string; +}; +``` + +`markdown:docs/common/control/popper-api.md` + +`markdown:docs/common/control/btn-api.md` + +`markdown:docs/common/control/api.md` + +## 方法 + +`markdown:docs/common/control/method.md` + +## 事件 + +`markdown:docs/common/control/event.md` + +`markdown:docs/common/control/popper-event.md` + +`markdown:docs/common/control/select-event.md` diff --git a/packages/site/docs/api/component/control/mouseLocation.zh.md b/packages/site/docs/api/component/control/mouseLocation.zh.md new file mode 100644 index 0000000000..adf7bcced7 --- /dev/null +++ b/packages/site/docs/api/component/control/mouseLocation.zh.md @@ -0,0 +1,49 @@ +--- +title: MouseLocation 光标经纬度 +order: 9 +--- + +用于实时展示当前光标在地图上所对应的经纬度。 + + + +## 使用 + +[示例](/zh/examples/component/control#mouselocation) + +```ts +import { Scene, MouseLocation } from '@antv/l7'; + +const scene = new Scene({ + // ... +}); + +scene.on('loaded', () => { + const mouseLocation = new MouseLocation({ + transform: (position) => { + return position; + }, + }); + scene.addControl(mouseLocation); +}); +``` + +## 配置 + +| 名称 | 说明 | 类型 | +| --------- | ---------------------------- | -------------------------------------------------- | +| transform | 转换光标所在经纬度的回调函数 | `(position: [number, number]) => [number, number]` | + +`markdown:docs/common/control/api.md` + +## 方法 + +`markdown:docs/common/control/method.md` + +## 事件 + +| 名称 | 说明 | 类型 | +| -------------- | ---------------------------- | -------------------------------------- | +| locationChange | 光标所在经纬度发生变化时触发 | `(position: [number, number]) => void` | + +`markdown:docs/common/control/event.md` diff --git a/packages/site/docs/api/component/control/scale.zh.md b/packages/site/docs/api/component/control/scale.zh.md new file mode 100644 index 0000000000..9221ee0027 --- /dev/null +++ b/packages/site/docs/api/component/control/scale.zh.md @@ -0,0 +1,48 @@ +--- +title: Scale 缩放尺 +order: 4 +--- + +该控件用于显示地图上的距离与地面上相应距离的比率。 + + + +## 使用 + +[示例](/zh/examples/component/control#scale) + +```ts +import { Scene, Scale } from '@antv/l7'; + +const scene = new Scene({ + //... +}); + +scene.on('loaded', () => { + const scale = new Scale({ + zoomInTitle: '放大', + zoomOutTitle: '缩小', + }); + scene.addControl(scale); +}); +``` + +## 配置 + +| 名称 | 说明 | 类型 | 默认值 | +| -------------- | ------------------------------------ | --------- | ------- | +| lockWidth | 是否固定容器宽度 | `boolean` | `true` | +| maxWidth | 组件的容器最大宽度 | `number` | `100` | +| metric | 展示**千米**格式的比例尺 | `boolean` | `true` | +| imperial | 展示**英里**格式的比例尺 | `boolean` | `false` | +| updateWhenIdle | 是否只在拖拽和缩放结束后才更新比例尺 | `boolean` | `false` | + +`markdown:docs/common/control/api.md` + +## 方法 + +`markdown:docs/common/control/method.md` + +## 事件 + +`markdown:docs/common/control/event.md` diff --git a/packages/site/docs/api/component/control/zoom.zh.md b/packages/site/docs/api/component/control/zoom.zh.md new file mode 100644 index 0000000000..e1487fd1c2 --- /dev/null +++ b/packages/site/docs/api/component/control/zoom.zh.md @@ -0,0 +1,52 @@ +--- +title: Zoom 缩放 +order: 3 +--- + +用于控制地图**放大**和**缩小**的控件,并且当地图达到最大或最小缩放比时,会禁用对应缩放按钮。 + + + +## 使用 + +[示例](/zh/examples/component/control#zoom) + +```ts +import { Scene, Zoom } from '@antv/l7'; + +const scene = new Scene({ + //... +}); + +scene.on('loaded', () => { + const zoom = new Zoom({ + zoomInTitle: '放大', + zoomOutTitle: '缩小', + }); + scene.addControl(zoom); +}); +``` + +## 配置 + +| 名称 | 说明 | 类型 | +| ------------ | ----------------------- | ------------------------- | +| zoomInText | 放大按钮的展示内容 | `Element` | `string` | +| zoomInTitle | 放大按钮的 `title` 属性 | `string` | +| zoomOutText | 缩小按钮的展示内容 | `Element` | `string` | +| zoomOutTitle | 缩小按钮的 `title` 属性 | `string` | + +`markdown:docs/common/control/api.md` + +## 方法 + +| 名称 | 说明 | 类型 | +| ------- | -------- | ------------ | +| zoomIn | 放大地图 | `() => void` | +| zoomOut | 缩小底图 | `() => void` | + +`markdown:docs/common/control/method.md` + +## 事件 + +`markdown:docs/common/control/event.md` diff --git a/packages/site/docs/api/component/marker.en.md b/packages/site/docs/api/component/marker/marker.en.md similarity index 100% rename from packages/site/docs/api/component/marker.en.md rename to packages/site/docs/api/component/marker/marker.en.md diff --git a/packages/site/docs/api/component/marker.zh.md b/packages/site/docs/api/component/marker/marker.zh.md similarity index 100% rename from packages/site/docs/api/component/marker.zh.md rename to packages/site/docs/api/component/marker/marker.zh.md diff --git a/packages/site/docs/api/component/markerLayer.en.md b/packages/site/docs/api/component/marker/markerLayer.en.md similarity index 100% rename from packages/site/docs/api/component/markerLayer.en.md rename to packages/site/docs/api/component/marker/markerLayer.en.md diff --git a/packages/site/docs/api/component/markerLayer.zh.md b/packages/site/docs/api/component/marker/markerLayer.zh.md similarity index 100% rename from packages/site/docs/api/component/markerLayer.zh.md rename to packages/site/docs/api/component/marker/markerLayer.zh.md diff --git a/packages/site/docs/api/component/popup.zh.md b/packages/site/docs/api/component/popup.zh.md deleted file mode 100644 index 77666ba09d..0000000000 --- a/packages/site/docs/api/component/popup.zh.md +++ /dev/null @@ -1,150 +0,0 @@ ---- -title: Popup 信息框 -order: 0 ---- - -`markdown:docs/common/style.md` - -地图标注信息窗口,用于展示地图要素的属性信息 - -## 构造函数 - -Popup - -```javascript -const option = {}; -const popup = new L7.Popup(option); -``` - -### option - -- closeButton 是否显示关闭按钮,布尔值,默认为 true。 -- closeButtonOffsets 显示关闭按钮时生效,[number, number],默认为 [0, 0],以右上角为起始点。 -- closeOnClick 是否在点击地图的时候关闭弹框,布尔值,默认为 true -- maxWidth 弹框最宽值,默认为 240px -- anchor 弹框锚点,默认值为 bottom,可选值有 center、top、top-left、left、bottom-left、bottom、bottom-right、right、top-right - -#### 添加到地图 - -```javascript -scene.addPopup(popup); -``` - -## 方法 - -#### setLnglat - -设置 popup 的经纬度位置 - -**参数**:lnglat - -支持数组 - -```javascript -[112, 32]; -``` - -经纬度对象 - -```javascript -const lnglat = { - lng: 112.323, - lat: 30.456, -}; -``` - -```javascript -popup.setLnglat([112, 32]); -``` - -#### setHTML - -**参数**:html 字符串 - -设置 popup html 内容 - -```javascript -var html = `

省份 - ${feature.s}

地区 - ${feature.m}

数值 - ${feature.t}

`; -popup.setHTML(html); -``` - -#### setDOMContent - -- 参数 htmlNode dom 对象 - 区别于 setHtml 对象只能传字符串 - -**tips** - -如果需要将 react 组件渲染到 popup 可以用此方法。 - -#### setText - -设置 popup 显示文本内容 - -```javascript -popup.setText('hello world'); -``` - -#### setMaxWidth - -设置 popup 最大宽度 - -```javascript -popup.setMaxWidth('300px'); -``` - -#### open - -显示 popup - -```javascript -popup.open(); -``` - -#### close - -关闭 popup - -```javascript -popup.close(); -``` - -#### remove - -移除 popup - -```javascript -popup.remove(); -``` - -## 事件 - -### open - -```javascript -popup.on('open', () => {}); -``` - -#### close - -```javascript -popup.on('close', () => {}); -``` - -## 示例代码 - -#### 添加 popup - -``` - var html = '

'+feature.m+'

'; - const popup= new L7.Popup().setLnglat([112, 32]).setHTML(html); - scene.addPopup(popup); -``` - -## demo 地址 - -[demo1](../../../examples/point/column#column_linear) -[demo2](../../../examples/line/path#bus_light) diff --git a/packages/site/docs/api/component/popup/layerPopup.zh.md b/packages/site/docs/api/component/popup/layerPopup.zh.md new file mode 100644 index 0000000000..902de99111 --- /dev/null +++ b/packages/site/docs/api/component/popup/layerPopup.zh.md @@ -0,0 +1,111 @@ +--- +title: LayerPopup 图层信息框 +order: 1 +--- + +LayerPopup 基于 Popup 封装的,专门用于展示图层元素信息的气泡。 + + + +## 说明 + +LayerPopup 是为了让开发者通过配置快速生成用于展示图层信息的气泡,开发者可以传入需要展示信息气泡的图层,以及需要展示的字段。 + +LayerPopup 会自行对目标图层的鼠标事件进行监听,当用户点击/悬停在目标图层的某一元素上时,会自动打开 Popup 并展示该元素的字段值。 + +## 使用 + +[示例](/zh/examples/component/popup#layerPopup) + +```ts +import { Scene, LayerPopup, PointLayer } from '@antv/l7'; + +const scene = new Scene({ + id: 'map', + map: new GaodeMapV2({ + // ... + }), +}); + +scene.on('loaded', () => { + const pointLayer = new PointLayer(); + pointLayer.source( + [ + { + lng: 120, + lat: 30, + name: 'Test 1', + }, + ], + { + parser: { + type: 'json', + x: 'lng', + y: 'lat', + }, + }, + ); + scene.addLayer(pointLayer); + const layerPopup = new LayerPopup({ + config: [ + { + layer: pointLayer, + fields: [ + { + field: 'name', + formatValue: (name?: string) => name.trim() ?? '-', + }, + ], + }, + ], + trigger: 'hover', + }); + scene.addPopup(popup); +}); +``` + +## 配置 + +| 名称 | 说明 | 类型 | 默认值 | +| ------- | --------------------------------------------------------------------------------------------- | ----------------------------- | --------- | +| config | 需要展示 Popup 的图层配置数组,每个选项类型可见 [LayerPopupConfigItem](#layerpopupconfigitem) | `Array` | `[]` | +| trigger | 鼠标触发 Popup 展示的方式 | `'hover' | 'click'` | `'hover'` | + +### LayerPopupConfigItem + +| 名称 | 说明 | 类型 | +| ------ | ------------------------------------------------------------------------------------------------- | --------------------- | +| layer | 需要展示 Popup 的目标图层实例,或其的 `id` 或 `name` | `BaseLayer | string` | +| fields | 需要展示的字段数组,支持传入字段 key 值字符串,或者针对该字段的详细配置 [LayerField](#layerfield) | `string | LayerField` | + +### LayerField + +| 名称 | 说明 | 类型 | +| ----------- | --------------------------- |-----------------------------| +| field | 字段的 key 值字符串 | `string` | +| formatField | 对展示的 key 字段进行格式化 | `(field: string) => string` | +| formatValue | 对展示的 value 值进行格式化 | `(value: any) => any` | +| getValue | 自定义获取值的方式 | `(feature: any) => any` | + +## 方法 + +| 名称 | 说明 | 类型 | +| ---------- | --------------------------- | ------------------------------------------------------------------- | +| getOptions | 获取当前 Popup 配置 | `() => IPopupOption` | +| setOptions | 更新当前 Popup 配置 | `(newOption: Partial) => this` | +| show | 显示 Popup | `() => this` | +| hide | 隐藏 Popup | `() => this` | +| getIsShow | 判断当前气泡是否展示 | `() => boolean` | +| setHTML | 设置 Popup 内容展示的 HTML | `(html: string | HTMLElement | HTMLElement[]) => this` | +| setText | 设置 Popup 内容展示的文本 | `(text: string) => this` | +| setLngLat | 设置 Popup 锚点所在经纬度 | `(lngLat: { lng: number; lat: number } | [number, number]) => this` | +| panToPopup | 将地图平移至当前 Popup 位置 | `() => this` | + +## 事件 + +| 名称 | 说明 | 类型 | +| ----- | ------------------ | ------------ | +| open | Popup 被添加时触发 | `() => void` | +| close | Popup 被移除时触发 | `() => void` | +| show | Popup 显示时触发 | `() => void` | +| hide | Popup 隐藏时触发 | `() => void` | diff --git a/packages/site/docs/api/component/popup.en.md b/packages/site/docs/api/component/popup/popup.en.md similarity index 100% rename from packages/site/docs/api/component/popup.en.md rename to packages/site/docs/api/component/popup/popup.en.md diff --git a/packages/site/docs/api/component/popup/popup.zh.md b/packages/site/docs/api/component/popup/popup.zh.md new file mode 100644 index 0000000000..da3ebede80 --- /dev/null +++ b/packages/site/docs/api/component/popup/popup.zh.md @@ -0,0 +1,114 @@ +--- +title: Popup 信息框 +order: 0 +--- + +Popup 是用于在地图上指定经纬度位置,展示自定义内容的气泡。 + + + +## 说明 + +Popup 的锚点位置是由经纬度来表达的,当地图缩放/平移时,Popup 会自动计算相对于当前地图的坐标并且自动位移。换句话说,如果开发者需要在地图的**指定经纬度位置**展示信息气泡,可以考虑使用 Popup 组件来实现对应效果。 + +开发者可以自定义 Popup 主体展示内容: + +- 纯文本可以通过 `text` 配置或者 `setText` 方法控制 Popup 的展示文本。 +- 自定义 DOM 可以通过 `html` 配置或者 `setHTML` 方法,支持传入 HTML 字符串或者 DOM 元素或数组的方式控制 Popup 的展示内容。 + +## 使用 + +[示例](/zh/examples/component/popup#popup) + +```ts +import { Scene, Popup } from '@antv/l7'; + +const scene = new Scene({ + id: 'map', + map: new GaodeMapV2({ + // ... + }), +}); + +scene.on('loaded', () => { + const popup = new Popup({ + // 初始锚点经纬度 + lngLat: { + lng: 120, + lat: 30, + }, + // Popup 标题 + title: 'Popup Title', + // Popup 内容 + html: 'Popup Content', + }); + scene.addPopup(popup); + + // 更新 Popup 锚点经纬度 + popup.setLngLat({ + lng: 130, + lat: 40, + }); + + // 更新 Popup 内容 + popup.setHTML('New Popup Content'); +}); +``` + +## 配置 + +| 名称 | 说明 | 类型 | 默认值 | +| ------------------ | ------------------------------------------------------------------------ | -------------------------------------- | ---------- | +| lngLat | Popup 所在的经纬度 | `{ lng: number; lat: number }` | - | +| text | Popup 内容展示的文本内容 | `string` | - | +| html | Popup 内容展示的自定义 HTML,可以传 HTML 字符串,也可以传 DOM 对象或数组 | `string | HTMLElement | HTMLElement[]` | - | +| title | Popup 标题展示的自定义 HTML,可以传 HTML 字符串,也可以传 DOM 对象或数组 | `string | HTMLElement | HTMLElement[]` | - | +| closeOnClick | 点击地图区域时,是否关闭当前 Popup | `boolean` | `true` | +| closeOnEsc | 点击 Esc 键时,是否关闭当前 Popup | `boolean` | `false` | +| maxWidth | Popup 的最大宽度 | `string` | `240px` | +| anchor | Popup 箭头位置,可以控制 Popup 相对于经纬度点的展示位置 | [AnchorType](#anchortype) | `'bottom'` | +| offsets | Popup 相对于锚点的偏移 | `[number, number]` | `[0, 0]` | +| autoPan | 当 Popup 展示或者位置发生变化时,地图是否要自动平移至 Popup 所在位置 | `boolean` | `false` | +| autoClose | 当有其他 Popup 展示时,是否自动关闭当前气泡 | `boolean` | `true` | +| followCursor | Popup 是否跟随光标移动,若设为 `true`,则 `lngLat` 配置无效 | `boolean` | `false` | +| closeButton | 是否展示关闭 Popup 图标 | `boolean` | `true` | +| closeButtonOffsets | 关闭 Popup 图标的相对偏移 | `[number, number]` | - | +| stopPropagation | Popup 上的鼠标事件是否要阻止其冒泡 | `boolean` | `true` | + +### AnchorType + +```ts +export type AnchorType = + | 'center' + | 'top' + | 'top-left' + | 'top-right' + | 'bottom' + | 'bottom-left' + | 'bottom-right' + | 'left' + | 'right'; +``` + +## 方法 + +| 名称 | 说明 | 类型 | +| ---------- | --------------------------- | ------------------------------------------------------------------- | +| getOptions | 获取当前 Popup 配置 | `() => IPopupOption` | +| setOptions | 更新当前 Popup 配置 | `(newOption: Partial) => this` | +| show | 显示 Popup | `() => this` | +| hide | 隐藏 Popup | `() => this` | +| getIsShow | 判断当前气泡是否展示 | `() => boolean` | +| setHTML | 设置 Popup 内容展示的 HTML | `(html: string | HTMLElement | HTMLElement[]) => this` | +| setText | 设置 Popup 内容展示的文本 | `(text: string) => this` | +| setLngLat | 设置 Popup 锚点所在经纬度 | `(lngLat: { lng: number; lat: number } | [number, number]) => this` | +| panToPopup | 将地图平移至当前 Popup 位置 | `() => this` | + +## 事件 + +| 名称 | 说明 | 类型 | +| ----- | ------------------ | ------------ | +| open | Popup 被添加时触发 | `() => void` | +| close | Popup 被移除时触发 | `() => void` | +| show | Popup 显示时触发 | `() => void` | +| hide | Popup 隐藏时触发 | `() => void` | diff --git a/packages/site/docs/api/point_layer/marker.zh.md b/packages/site/docs/api/point_layer/marker.zh.md index 62f3e2cd11..8f0a38d7bf 100644 --- a/packages/site/docs/api/point_layer/marker.zh.md +++ b/packages/site/docs/api/point_layer/marker.zh.md @@ -15,6 +15,6 @@ import { MarkerLayer, Marker } from '@antv/l7'; 案例 -[Marker 文档](../component/marker) +[Marker 文档](../component/marker/marker) -[MarkerLayer 文档](../component/markerLayer) +[MarkerLayer 文档](../component/marker/markerLayer) diff --git a/packages/site/docs/common/control/api.md b/packages/site/docs/common/control/api.md new file mode 100644 index 0000000000..1f3e179e6f --- /dev/null +++ b/packages/site/docs/common/control/api.md @@ -0,0 +1,23 @@ +| 名称 | 说明 | 类型 | +| --------- | ---------------------------------------------------------------------------------------------- | --------------------- | +| position | 控件被添加到地图中的位置以及排列方式,详情可见 [控件插槽](/zh/docs/api/component/control/control#插槽) | [Position](#position) | +| className | 自定义样式名 | `string` | +| style | 自定义样式 | `string` | + +### Position + +```ts +export type Position = + | 'topleft' // ↖ 左上角,纵向排列 + | 'lefttop' // ↖ 左上角,横向排列 + | 'topright' // ↗ 右上角,纵向排列 + | 'righttop' // ↗ 右上角,横向排列 + | 'bottomleft' // ↙ 左下角,纵向排列 + | 'leftbottom' // ↙ 左下角,横向排列 + | 'bottomright' // ↘ 右下角,纵向排列 + | 'rightbottom' // ↘ 右下角,横向排列 + | 'topcenter' // ↑ 上方中央,横向排列 + | 'bottomcenter' // ↓ 下方中间,横向排列 + | 'leftcenter' // ← 左边中间,纵向排列 + | 'rightcenter'; // → 右边中间,纵向排列 +``` diff --git a/packages/site/docs/common/control/btn-api.md b/packages/site/docs/common/control/btn-api.md new file mode 100644 index 0000000000..29296194ce --- /dev/null +++ b/packages/site/docs/common/control/btn-api.md @@ -0,0 +1,6 @@ +| 名称 | 说明 | 类型 | +| -------- | ------------------------------------------------------- | --------------------------------- | +| btnIcon | 按钮图标 | `HTMLElement` | `SVGElement` | +| btnText | 按钮内容文本 | `string` | +| title | 按钮的 `title` 属性 | `string` | +| vertical | 在 btnIcon 有值的情况下,按钮内的图标和文案是否纵向排列 | `boolean` | diff --git a/packages/site/docs/common/control/event.md b/packages/site/docs/common/control/event.md new file mode 100644 index 0000000000..f434552cea --- /dev/null +++ b/packages/site/docs/common/control/event.md @@ -0,0 +1,6 @@ +| 名称 | 说明 | 类型 | +| ------ | ------------------ | ---------------- | +| add | 组件被添加时的事件 | `(this) => void` | +| remove | 组件被移除时的事件 | `(this) => void` | +| show | 组件显示时的事件 | `(this) => void` | +| hide | 组件隐藏时的事件 | `(this) => void` | diff --git a/packages/site/docs/common/control/method.md b/packages/site/docs/common/control/method.md new file mode 100644 index 0000000000..059d379d81 --- /dev/null +++ b/packages/site/docs/common/control/method.md @@ -0,0 +1,7 @@ +| 名称 | 说明 | 类型 | +| ------------ | --------------------------------------------- | -------------------------------------- | +| setOptions | 更新配置,参数需要参考对应组件的[配置](#配置) | `(newOption: Partial