Merge pull request #229 from antvis/control

Control
This commit is contained in:
@thinkinggis 2020-02-28 14:19:48 +08:00 committed by GitHub
commit 50a7a1cb2f
23 changed files with 384 additions and 419 deletions

File diff suppressed because one or more lines are too long

View File

@ -2,6 +2,7 @@ import Ajv from 'ajv';
import { injectable, postConstruct } from 'inversify';
import { merge } from 'lodash';
import { ILayerConfig } from '../layer/ILayerService';
import { IRenderConfig } from '../renderer/IRendererService';
import { IGlobalConfigService, ISceneConfig } from './IConfigService';
import mapConfigSchema from './mapConfigSchema';
import sceneConfigSchema from './sceneConfigSchema';
@ -10,10 +11,12 @@ import WarnInfo, { IWarnInfo } from './warnInfo';
/**
*
*/
const defaultSceneConfig: Partial<ISceneConfig> = {
const defaultSceneConfig: Partial<ISceneConfig & IRenderConfig> = {
id: 'map',
logoPosition: 'bottomleft',
logoVisible: true,
antialias: true,
preserveDrawingBuffer: false,
};
/**

View File

@ -62,6 +62,7 @@ export interface IMapService<RawMap = {}> {
lngLatToPixel(lnglat: Point): IPoint;
containerToLngLat(pixel: Point): ILngLat;
lngLatToContainer(lnglat: Point): IPoint;
exportMap(type: 'jpg' | 'png'): string;
}
export const MapServiceEvent = ['mapload'];

View File

@ -16,6 +16,8 @@ export interface IRenderConfig {
*/
enableMultiPassRenderer?: boolean;
passes?: Array<IPass<unknown>>;
antialias?: boolean;
preserveDrawingBuffer?: boolean;
}
export interface IClearOptions {
@ -40,7 +42,7 @@ export interface IReadPixelsOptions {
}
export interface IRendererService {
init($container: HTMLDivElement): Promise<void>;
init($container: HTMLDivElement, cfg: IRenderConfig): Promise<void>;
clear(options: IClearOptions): void;
createModel(options: IModelInitializationOptions): IModel;
createAttribute(options: IAttributeInitializationOptions): IAttribute;

View File

@ -1,3 +1,4 @@
import { ISceneConfig } from '../config/IConfigService';
import { ILayer } from '../layer/ILayerService';
import { IMapConfig } from '../map/IMapService';
import { IRenderConfig } from '../renderer/IRendererService';
@ -8,9 +9,10 @@ export interface ISceneService {
removeAllListeners(event?: string): this;
init(config: IMapConfig & IRenderConfig): void;
addLayer(layer: ILayer): void;
getSceneConfig(): Partial<ISceneConfig>;
render(): void;
getSceneContainer(): HTMLDivElement;
exportPng(): string;
exportPng(type?: 'png' | 'jpg'): string;
destroy(): void;
}
// scene 事件

View File

@ -18,8 +18,8 @@ import { IInteractionService } from '../interaction/IInteractionService';
import { IPickingService } from '../interaction/IPickingService';
import { ILayer, ILayerService } from '../layer/ILayerService';
import { ILogService } from '../log/ILogService';
import { IMapCamera, IMapService } from '../map/IMapService';
import { IRendererService } from '../renderer/IRendererService';
import { IMapCamera, IMapConfig, IMapService } from '../map/IMapService';
import { IRenderConfig, IRendererService } from '../renderer/IRendererService';
import { IShaderModuleService } from '../shader/IShaderModuleService';
import { ISceneService } from './ISceneService';
@ -169,7 +169,10 @@ export default class Scene extends EventEmitter implements ISceneService {
);
this.$container = $container;
if ($container) {
await this.rendererService.init($container);
await this.rendererService.init(
$container,
this.configService.getSceneConfig(this.id) as IRenderConfig,
);
elementResizeEvent(
this.$container as HTMLDivElement,
this.handleWindowResized,
@ -231,13 +234,20 @@ export default class Scene extends EventEmitter implements ISceneService {
return this.$container as HTMLDivElement;
}
public exportPng(): string {
public exportPng(type?: 'png' | 'jpg'): string {
const renderCanvas = this.$container?.getElementsByTagName('canvas')[0];
this.render();
const layersPng = renderCanvas?.toDataURL('image/png') as string;
const layersPng =
type === 'jpg'
? (renderCanvas?.toDataURL('image/jpeg') as string)
: (renderCanvas?.toDataURL('image/png') as string);
return layersPng;
}
public getSceneConfig(): Partial<ISceneConfig> {
return this.configService.getSceneConfig(this.id as string);
}
public destroy() {
this.emit('destroy');
this.inited = false;

View File

@ -875,6 +875,11 @@ export default class BaseLayer<ChildLayerStyleOptions = {}> extends EventEmitter
private sourceEvent = () => {
this.dataState.dataSourceNeedUpdate = true;
const { autoFit } = this.getLayerConfig();
if (autoFit) {
this.fitBounds();
}
this.emit('dataUpdate');
this.reRender();
};

View File

@ -22,7 +22,7 @@ export default class UpdateStyleAttributePlugin implements ILayerPlugin {
}: { styleAttributeService: IStyleAttributeService },
) {
layer.hooks.init.tap('UpdateStyleAttributePlugin', () => {
this.updateStyleAtrribute(layer, { styleAttributeService });
this.initStyleAttribute(layer, { styleAttributeService });
});
layer.hooks.beforeRenderData.tap('styleAttributeService', () => {
@ -70,4 +70,26 @@ export default class UpdateStyleAttributePlugin implements ILayerPlugin {
);
});
}
private initStyleAttribute(
layer: ILayer,
{
styleAttributeService,
}: { styleAttributeService: IStyleAttributeService },
) {
const attributes = styleAttributeService.getLayerStyleAttributes() || [];
attributes
.filter((attribute) => attribute.needRegenerateVertices)
.forEach((attribute) => {
// 精确更新某个/某些 feature(s),需要传入 featureIdx d
styleAttributeService.updateAttributeByFeatureRange(
attribute.name,
layer.getEncodedData(), // 获取经过 mapping 最新的数据
attribute.featureRange.startIndex,
attribute.featureRange.endIndex,
);
attribute.needRegenerateVertices = false;
this.logger.debug(`init vertex attributes: ${attribute.name} finished`);
});
}
}

View File

@ -85,6 +85,7 @@ export default class TextModel extends BaseModel {
private currentZoom: number = -1;
private extent: [[number, number], [number, number]];
private textureHeight: number = 0;
private textCount: number = 0;
private preTextStyle: Partial<IPointTextLayerStyleOptions> = {};
private glyphInfoMap: {
[key: string]: {
@ -101,10 +102,10 @@ export default class TextModel extends BaseModel {
strokeWidth = 0,
strokeOpacity = 1,
textAnchor = 'center',
textAllowOverlap = true,
textAllowOverlap = false,
} = this.layer.getLayerConfig() as IPointTextLayerStyleOptions;
const { canvas } = this.fontService;
if (canvas.height !== this.textureHeight) {
const { canvas, mapping } = this.fontService;
if (Object.keys(mapping).length !== this.textCount) {
this.updateTexture();
}
this.preTextStyle = {

View File

@ -63,6 +63,7 @@ export default function(
points: number[][],
closed: boolean,
indexOffset: number,
isDash: boolean = true,
) {
const lineA = vec2.fromValues(0, 0);
const lineB = vec2.fromValues(0, 0);
@ -108,8 +109,11 @@ export default function(
: null;
}
}
const lineDistance = lineSegmentDistance(cur, last);
const d = lineDistance + attrDistance[attrDistance.length - 1];
let d = 0;
if (isDash) {
const lineDistance = lineSegmentDistance(cur, last);
d = lineDistance + attrDistance[attrDistance.length - 1];
}
direction(lineA, cur, last);
if (!lineNormal) {
lineNormal = vec2.create();
@ -218,15 +222,15 @@ export default function(
attrPos[i * 3],
attrPos[i * 3 + 1],
attrPos[i * 3 + 2],
attrDistance[i],
attrDistance[i], // dash
miters[i],
totalDistance,
totalDistance, // dash
);
}
return {
normals: out,
attrIndex,
attrPos: pickData, // [x,y,z, distance, miter ]
attrPos: pickData, // [x,y,z, distance, miter ,tatal ]
};
}
// [x,y,z, distance, miter ]

View File

@ -305,6 +305,18 @@ export default class AMapService
this.viewport = new Viewport();
}
public exportMap(type: 'jpg' | 'png'): string {
const renderCanvas = this.getContainer()?.getElementsByClassName(
'amap-layer',
)[0] as HTMLCanvasElement;
const layersPng =
type === 'jpg'
? (renderCanvas?.toDataURL('image/jpeg') as string)
: (renderCanvas?.toDataURL('image/png') as string);
return layersPng;
}
public emit(name: string, ...args: any[]) {
this.eventEmitter.emit(name, ...args);
}

View File

@ -274,6 +274,14 @@ export default class MapboxService
return this.$mapContainer;
}
public exportMap(type: 'jpg' | 'png'): string {
const renderCanvas = this.map.getCanvas();
const layersPng =
type === 'jpg'
? (renderCanvas?.toDataURL('image/jpeg') as string)
: (renderCanvas?.toDataURL('image/png') as string);
return layersPng;
}
public onCameraChanged(callback: (viewport: IViewport) => void): void {
this.cameraChangedCallback = callback;
}

View File

@ -10,6 +10,14 @@ export const MapTheme: {
glyphs:
'https://gw.alipayobjects.com/os/antvdemo/assets/mapbox/glyphs/{fontstack}/{range}.pbf',
sources: {},
layers: [],
layers: [
{
id: 'background',
type: 'background',
layout: {
visibility: 'none',
},
},
],
},
};

View File

@ -1,4 +1,4 @@
import { IMapConfig, Scene } from '@antv/l7';
import { IMapConfig, ISceneConfig, Scene } from '@antv/l7';
// @ts-ignore
// tslint:disable-next-line:no-submodule-imports
import GaodeMap from '@antv/l7-maps/lib/amap';
@ -7,20 +7,26 @@ import { SceneContext } from './SceneContext';
interface IMapSceneConig {
style?: React.CSSProperties;
className?: string;
map: IMapConfig;
map: Partial<IMapConfig>;
option?: Partial<ISceneConfig>;
children?: JSX.Element | JSX.Element[] | Array<JSX.Element | undefined>;
onSceneLoaded?: (scene: Scene) => void;
}
const AMapScene = React.memo((props: IMapSceneConig) => {
const { style, className, map } = props;
const { style, className, map, option, onSceneLoaded } = props;
const container = createRef();
const [scene, setScene] = useState<Scene>();
useEffect(() => {
const sceneInstance = new Scene({
id: container.current as HTMLDivElement,
...option,
map: new GaodeMap(map),
});
sceneInstance.on('loaded', () => {
setScene(sceneInstance);
if (onSceneLoaded) {
onSceneLoaded(sceneInstance);
}
});
return () => {
sceneInstance.destroy();

View File

@ -9,7 +9,7 @@ const PolygonLayer = React.memo(function Layer(
});
const LineLayer = React.memo(function Layer(props: ILayerProps) {
return BaseLayer('polygonLayer', props);
return BaseLayer('lineLayer', props);
});
const PointLayer = React.memo(function Layer(

View File

@ -1,4 +1,4 @@
import { IMapConfig, Scene, Zoom } from '@antv/l7';
import { IMapConfig, ISceneConfig, Scene, Zoom } from '@antv/l7';
// @ts-ignore
// tslint:disable-next-line:no-submodule-imports
import Mapbox from '@antv/l7-maps/lib/mapbox';
@ -7,11 +7,13 @@ import { SceneContext } from './SceneContext';
interface IMapSceneConig {
style?: React.CSSProperties;
className?: string;
map: IMapConfig;
map: Partial<IMapConfig>;
option?: Partial<ISceneConfig>;
children?: JSX.Element | JSX.Element[] | Array<JSX.Element | undefined>;
onSceneLoaded?: (scene: Scene) => void;
}
const MapboxScene = React.memo((props: IMapSceneConig) => {
const { style, className, map } = props;
const { style, className, map, option, onSceneLoaded } = props;
const container = createRef();
const [scene, setScene] = useState<Scene>();
@ -19,10 +21,14 @@ const MapboxScene = React.memo((props: IMapSceneConig) => {
useEffect(() => {
const sceneInstance = new Scene({
id: container.current as HTMLDivElement,
...option,
map: new Mapbox(map),
});
sceneInstance.on('loaded', () => {
setScene(sceneInstance);
if (onSceneLoaded) {
onSceneLoaded(sceneInstance);
}
});
return () => {
sceneInstance.destroy();

View File

@ -15,6 +15,7 @@ import {
IModel,
IModelInitializationOptions,
IReadPixelsOptions,
IRenderConfig,
IRendererService,
ITexture2D,
ITexture2DInitializationOptions,
@ -36,7 +37,10 @@ export default class ReglRendererService implements IRendererService {
private gl: regl.Regl;
private $container: HTMLDivElement | null;
public async init($container: HTMLDivElement): Promise<void> {
public async init(
$container: HTMLDivElement,
cfg: IRenderConfig,
): Promise<void> {
this.$container = $container;
// tslint:disable-next-line:typedef
this.gl = await new Promise((resolve, reject) => {
@ -46,9 +50,9 @@ export default class ReglRendererService implements IRendererService {
alpha: true,
// use TAA instead of MSAA
// @see https://www.khronos.org/registry/webgl/specs/1.0/#5.2.1
antialias: true,
antialias: cfg.antialias,
premultipliedAlpha: true,
preserveDrawingBuffer: false,
preserveDrawingBuffer: cfg.preserveDrawingBuffer,
},
// TODO: use extensions
extensions: [

View File

@ -57,7 +57,7 @@ class Scene
private container: Container;
public constructor(config: ISceneConfig) {
const { id, map, logoPosition, logoVisible } = config;
const { id, map, logoPosition, logoVisible = true } = config;
// 创建场景容器
const sceneContainer = createSceneContainer();
this.container = sceneContainer;
@ -92,16 +92,15 @@ class Scene
// 初始化 scene
this.sceneService.init(config);
// TODO: 初始化组件
if (logoVisible) {
this.addControl(new Logo({ position: logoPosition }));
}
this.initControl();
}
public getMapService(): IMapService<unknown> {
return this.mapService;
}
public exportPng(): string {
return this.sceneService.exportPng();
public exportPng(type?: 'png' | 'jpg'): string {
return this.sceneService.exportPng(type);
}
public get map() {
@ -306,6 +305,13 @@ class Scene
this.markerService.init(this.container);
this.popupService.init(this.container);
}
private initControl() {
const { logoVisible, logoPosition } = this.sceneService.getSceneConfig();
if (logoVisible) {
this.addControl(new Logo({ position: logoPosition }));
}
}
// 资源管理
}

View File

@ -1,5 +1,5 @@
// @ts-ignore
import { Layers, PointLayer, PolygonLayer, Scale, Scene } from '@antv/l7';
import { Layers, PointLayer, PolygonLayer, Scale, Scene, Zoom } from '@antv/l7';
import { Mapbox } from '@antv/l7-maps';
import * as React from 'react';
@ -21,6 +21,7 @@ export default class ScaleComponent extends React.Component {
const data = await response.json();
const scene = new Scene({
id: 'map',
logoVisible: false,
map: new Mapbox({
style: 'dark',
center: [110.19382669582967, 30.258134],
@ -79,10 +80,15 @@ export default class ScaleComponent extends React.Component {
};
const layerControl = new Layers({
overlayers: layers,
position: 'bottomright',
});
scene.addControl(scaleControl);
scene.addControl(layerControl);
const zoomControl = new Zoom({
position: 'bottomright',
});
scene.addControl(zoomControl);
}
public render() {

View File

@ -16,6 +16,7 @@ import Point3D from './components/Point3D';
import PointImage from './components/PointImage';
import PolygonDemo from './components/polygon';
import Polygon3D from './components/Polygon3D';
import WorldDemo from './components/polygon_line';
import ImageLayerDemo from './components/RasterImage';
import RasterLayerDemo from './components/RasterLayer';
import TextLayerDemo from './components/Text';
@ -40,4 +41,5 @@ storiesOf('图层', module)
.add('热力图', () => <HeatMapDemo />)
.add('网格热力图', () => <HexagonLayerDemo />)
.add('栅格', () => <RasterLayerDemo />)
.add('图片', () => <ImageLayerDemo />);
.add('图片', () => <ImageLayerDemo />)
.add('世界地图', () => <WorldDemo />);

View File

@ -0,0 +1,111 @@
// @ts-ignore
import {
Layers,
LineLayer,
PointLayer,
PolygonLayer,
Scale,
Scene,
Zoom,
} from '@antv/l7';
import { Mapbox } from '@antv/l7-maps';
import * as React from 'react';
export default class World extends React.Component {
private scene: Scene;
public componentWillUnmount() {
this.scene.destroy();
}
public async componentDidMount() {
const response = await fetch(
'https://gw.alipayobjects.com/os/basement_prod/68dc6756-627b-4e9e-a5ba-e834f6ba48f8.json',
);
const response2 = await fetch(
'https://gw.alipayobjects.com/os/basement_prod/13b3aa35-f7a1-4d21-acad-805a4553edb4.json',
);
const pointsData = await response2.json();
const data = await response.json();
const scene = new Scene({
id: 'map',
logoVisible: false,
map: new Mapbox({
style: 'dark',
center: [110.19382669582967, 30.258134],
pitch: 0,
zoom: 0,
}),
});
this.scene = scene;
const layer = new PolygonLayer({
name: '01',
});
layer
.source(data)
.color('name', [
'#2E8AE6',
'#69D1AB',
'#DAF291',
'#FFD591',
'#FF7A45',
'#CF1D49',
])
.shape('fill')
.select(true)
.style({
opacity: 1.0,
});
scene.addLayer(layer);
const linelayer = new LineLayer({
name: '01',
});
linelayer
.source(data)
.color('#fff')
.size(1)
.shape('line')
.select(true)
.style({
opacity: 1.0,
});
scene.addLayer(linelayer);
const pointLayer = new PointLayer({
name: '02',
})
.source(pointsData, {
parser: {
type: 'json',
coordinates: 'center',
},
})
.shape('name', 'text')
.size(12)
.active(true)
.color('#fff')
.style({
opacity: 1,
sttoke: '#FFF',
strokeWidth: 0,
});
scene.addLayer(pointLayer);
}
public render() {
return (
<div
id="map"
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
}}
/>
);
}
}

View File

@ -1,6 +1,8 @@
import { storiesOf } from '@storybook/react';
import * as React from 'react';
import GaodeMapScene from './components/Scene';
import WorldMap from './components/world';
// @ts-ignore
storiesOf('React', module).add('高德地图', () => <GaodeMapScene />);
storiesOf('React', module).add('世界地图', () => <WorldMap />);

View File

@ -0,0 +1,76 @@
import { LineLayer, MapboxScene, PolygonLayer } from '@antv/l7-react';
import * as React from 'react';
export default React.memo(function Map() {
const [data, setData] = React.useState();
React.useEffect(() => {
const fetchData = async () => {
const response = await fetch(
'https://gw.alipayobjects.com/os/basement_prod/68dc6756-627b-4e9e-a5ba-e834f6ba48f8.json',
);
const data = await response.json();
setData(data);
};
fetchData();
}, []);
return (
<>
<MapboxScene
map={{
center: [110.19382669582967, 50.258134],
pitch: 0,
style: 'blank',
zoom: 1,
}}
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
}}
>
{data && [
<PolygonLayer
key={'2'}
source={{
data,
}}
color={{
field: 'name',
values: [
'#2E8AE6',
'#69D1AB',
'#DAF291',
'#FFD591',
'#FF7A45',
'#CF1D49',
],
}}
shape={{
values: 'fill',
}}
style={{
opacity: 1,
}}
/>,
<LineLayer
key={'21'}
source={{
data,
}}
color={{
values: '#fff',
}}
shape={{
values: 'line',
}}
style={{
opacity: 1,
}}
/>,
]}
</MapboxScene>
</>
);
});