(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,iVBORw0KGgoAAAANSUhEUgAAACAAAAAkCAYAAAGf7Ah0AAAABGdBTUEAALGPC/xhBQAABzdJREFUWAnVV3lslEUUn/l2F0tpwdAih1HEqCReDcphNCo1EkgFkXa5PNpuC0Fo2e7Wgko8KiaKWrvbK5672xYFpNtiRRuNiY0aURA14BGviBcVlaKltVa6+42/N9v5Mrtt05rwj/PHzjt+8743b+a9N8vYwKgMNlUQyemnMtAkaJajvb3dToQSGp8e6boopkr4VYgEMYwO0vhCTfMVzBcMf8bx086YyODcuM80zTqp9IWat+7evW8sMdIxIqqCLa6oiAaJpuEtcBqcc2HEWMZ0JclgZR/Nchf++ubrzKjJSguXS4tVjXvPjfb3/UAAacGTn/MOMb7QnjNpNqP/bKZ50Ai0tqYmCi0n4zbM+W+lBc7JBOa+QPhDwcTsxJXE2+32edKCL9h8hRDmRzpIOWydAylhrcMfDN+ugBRKSdP3KwPhsFJIPth02FISIYQwYsDYcVfWhy/Bon4JgumffMGmiGTwQ0B/sHml4q2ZFAD3WQIQVhx0IdFw8B189lolN+w8w5PnjPmlhJgtA8/vfn3iye7u3yGzDljDxZEG4097Cp3rSSjBtMHunp6vFR+HHoIxmbgTe1xHKssDHecLNFUjMzbqMs5ZlHF7ute17M84+TMHDzr+OvR9JwKdCms9yRkzJq6bPTsWXh05QOPUN+PUHyPWMPhm6UHd7vaUohWZPSRE8OqxpTyibQa/qcTlbEP0vwF7AWUAT3JM9dy69FfSNza+MU4awNE8iQtXSkJuGLleV852ov2h5jkwVof0mUs8aO4LhTuYYFOAjExId0wcFIPahpaLTkWiX9ECfHG/sLNi1i8+HODfgLFFRI96+INNi0cNTgTS1obKQh03aAuk9DeELzcj4pACYivvwvXrFK/PcQZiQWo+hmidpYMUjQC7EOB6xdNsGfAHwk/RDdOVw9Dm+NTUSWtWLDohDVQ37JkbiUT2DwMeVswZP+gtdM4x3HnLDhjcGM2XLWOISaenIGee9MCSgqgMht/H/q/SZYk0qviV3oKcj5XcioESyOoqIseFYDYloxnAGm/hcrcuk3JV2XBMl+pKqn6mENux11+w12m6jpqMMFn7rPPTHdwfCm8yTfG4tMaNu+GepPUFiqbM7T105ARSPQU+dY/LOC/N2gI65WEkyWUEtjmSppfkLvlRLaQ5VnZFDtGGzbheNRROKZmbu/AvUvh3tE4Wff2/0IUC+y2q+4VVoXBW1BSvkR7Rb8BW84lWJQBdlGozcs7GLy3Nd35OSqTxBqtfgkc1+sPjcqbDgEl6xK0dH5lv545LOCVL1/F+3CoY4eyY1+WcBiC2KYEHQBd5XDkyndE17xCm2Ug6BLcSwb3LigE1AlOYu6TS4HUwVEw0Df1osfBnLDwnpiFDCQPuvQ73Fkqxg8/hEVYLXt66MXbbzOK8bKre1hhkgDT0RDja3fEHytwZxCMLH0YWPkD0fxqjqUZDejDSV3z1LbexqPkEPJxKWLqtKOGbvPnZL460NlE/KgfkIZiRrfjShsQcG2QQDUwwXjcuZcID61Ys6ErUJ/LDOiCfLUzUjVQdEg0O4jn/ABEq0iuIjrEcwE0x/KGWNbjPj4BO00Gni0ZOoIPzLR5X9vMqqaQDqDzzENra4V5zp8sBZQcROYjsLvYUOPdbEVBKeiZ193QjCmwtZCM+ldS6EWYTH3wuNSV1i+pFCj/IAaWgGUfBq0N78qPM3DZcp9TxcTQeyzZm3ON2LatXpSlOP8BIB6qDLddHmHnFrBlpNZmZmdZbNXEB9XsRlZXFennqGHzoXW5DaId4iSoc/Q365EjnRjszPnYXZL9NDWUtGsqzCjAw7xprTypbn7fkaILcYqkInzzeX06C8emO8sKlS7stZQLxVMPes/+O9FVAvEpXob+utY6gZmfrtMjf/RUI+2odhF7wJbq+Gzf3zTj5CAwyaoEwozVoCzN1KKK00z7WUbZx9dIOklsO6CAZpu87i5gpHoJ8gtLh9vZixSO2KckV7qysf5Sc5uq2tjOix3rL0FW3IJuSNV0XM/iDs85LqxvqeKUD8n3LxUobt5WVuLJlS9IM4O9i09VRwWtxLWfpcix+hXjs8mZdjn19YuOiuKRgufzXqOuqQi2r8O+yAk/tl2Q/RMW7ETe8UdX1GJhHDM5qjDHJ5e7bs07qBp7ZsTe9t6/vUXykkLKEdLFbLgLJSUn3rrt1yXEdX/1C23jzVG+5Keg/B5r2wJD9g/NcaUAJq4MvXxxl/TWoATcoGc0Av4eUKnIXZlsvXpLHO0CS2KgOtGQgdeuwqWuUjGbUgrdszLHRXXDLF0oe54AS0kyPnc5Iz/2CmV7EeIzSwcjvsHQPXkUhld/kCP6Xu+DRNjg/SWHh+SnODF+aPeVh9XCydAPEsA4kAqsCzcuiTPiw7+maTj6SwGsVk/9gY9xbUpizR8MNS47aAd0C7s35CG8VdrxYyjl/FcdUgo73nY77X9D/Au9RDMh+aBoVAAAAAElFTkSuQmCC');
- 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