diff --git a/demo/index.ts b/demo/index.ts index 76fc17a..3cbc863 100644 --- a/demo/index.ts +++ b/demo/index.ts @@ -6,7 +6,7 @@ import { REALTY_SCENE, REALTY_SCENE_1 } from './mocks'; let isDarkTheme = false; async function start() { - const mapglAPI = await load('https://mapgl.2gis.com/api/js/v0.0.322'); + const mapglAPI = await load(); const map = new mapglAPI.Map('container', { center: [47.245286302641034, 56.134743473834099], @@ -20,19 +20,11 @@ async function start() { (window as any).map = map; const plugin = new GltfPlugin(map, { - modelsLoadStrategy: 'waitAll', + modelsLoadStrategy: 'dontWaitAll', modelsBaseUrl: 'https://disk.2gis.com/digital-twin/models_s3/realty_ads/zgktechnology/', floorsControl: { position: 'centerRight' }, - poiConfig: { - primary: { - fontSize: 14, - }, - secondary: { - fontSize: 14, - }, - }, - hoverHighlight: { - intencity: 0.1, + hoverOptions: { + color: '#FFF3F3', }, groundCoveringColor: 'rgba(0, 0, 0, 0.8)', }); diff --git a/demo/mocks.ts b/demo/mocks.ts index 7281603..963a2ae 100644 --- a/demo/mocks.ts +++ b/demo/mocks.ts @@ -31,46 +31,68 @@ export const REALTY_SCENE: BuildingOptions[] = [ rotation: -57.5, }, isUnderground: true, - poiGroups: [ + labelGroups: [ { id: '1111', - type: 'primary', + image: { + url: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjgiIGhlaWdodD0iMjgiIHZpZXdCb3g9IjAgMCAyOCAyOCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMjgiIGhlaWdodD0iMjgiIHJ4PSI0IiBmaWxsPSIjZWFlYWVhIi8+PHJlY3QgeD0iMSIgeT0iMSIgd2lkdGg9IjI2IiBoZWlnaHQ9IjI2IiByeD0iMyIgZmlsbD0id2hpdGUiLz48L3N2Zz4=', + size: [38, 38], + stretchX: [[4, 24]], + stretchY: [[4, 24]], + padding: [5, 10, 5, 10], + }, minZoom: 19.5, elevation: 5, fontSize: 12, fontColor: '#3a3a3a', - data: [ + labels: [ { coordinates: [47.245048150280994, 56.134470449142164], - label: '3к\n78.4 м²', + text: '3к\n78.4 м²', userData: { url: 'https://a101.ru/kvartiry/360810/', }, }, { coordinates: [47.24520807647288, 56.13443854463778], - label: '2к\n67 м²', + text: '2к\n67 м²', userData: { url: 'https://a101.ru/kvartiry/360810/', }, }, + ], + }, + { + id: '2222', + image: { + url: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAiIGhlaWdodD0iMzAiIHZpZXdCb3g9IjAgMCAzMCAzMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMzAiIGhlaWdodD0iMzAiIHJ4PSI1IiBmaWxsPSIjNEZBQjhBIi8+PHJlY3QgeD0iMSIgeT0iMSIgd2lkdGg9IjI4IiBoZWlnaHQ9IjI4IiByeD0iNCIgZmlsbD0id2hpdGUiLz48cmVjdCB4PSIyIiB5PSIyIiB3aWR0aD0iMjYiIGhlaWdodD0iMjYiIHJ4PSIzIiBmaWxsPSIjNEZBQjhBIi8+PC9zdmc+', + size: [38, 38], + stretchX: [[4, 24]], + stretchY: [[4, 24]], + padding: [5, 10, 5, 10], + }, + minZoom: 19.5, + elevation: 5, + fontSize: 12, + fontColor: '#3a3a3a', + labels: [ { coordinates: [47.245350349632965, 56.134414208205776], - label: '1к\n40 м²', + text: '1к\n40 м²', userData: { url: 'https://a101.ru/kvartiry/360810/', }, }, { coordinates: [47.24542896512635, 56.13448965532694], - label: '3к\n90 м²', + text: '3к\n90 м²', userData: { url: 'https://a101.ru/kvartiry/360810/', }, }, { coordinates: [47.24510451854659, 56.134541185948585], - label: '3к\n77.2 м²', + text: '3к\n77.2 м²', userData: { url: 'https://a101.ru/kvartiry/360810/', }, @@ -89,46 +111,52 @@ export const REALTY_SCENE: BuildingOptions[] = [ zoom: 19.5, rotation: -62.6, }, - poiGroups: [ + labelGroups: [ { id: '1111', - type: 'primary', + image: { + url: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjRweCIgaGVpZ2h0PSIyNHB4IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGcgaWQ9IlBhZ2UtMSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+PGcgaWQ9ImV4aXQiIGZpbGw9IiM2MDVFNTAiIGZpbGwtcnVsZT0ibm9uemVybyI+PHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiByeD0iNSI+PC9yZWN0PjwvZz48L2c+PC9zdmc+', + size: [38, 38], + stretchX: [[4, 24]], + stretchY: [[4, 24]], + padding: [5, 10, 5, 10], + }, minZoom: 19, elevation: 35, fontSize: 12, - fontColor: '#3a3a3a', - data: [ + fontColor: '#fff', + labels: [ { coordinates: [47.245048150280994, 56.134470449142164], - label: '3к\n78.4 м²', + text: '3к\n78.4 м²', userData: { url: 'https://a101.ru/kvartiry/360810/', }, }, { coordinates: [47.24520807647288, 56.13443854463778], - label: '2к\n67 м²', + text: '2к\n67 м²', userData: { url: 'https://a101.ru/kvartiry/360810/', }, }, { coordinates: [47.245350349632965, 56.134414208205776], - label: '1к\n40 м²', + text: '1к\n40 м²', userData: { url: 'https://a101.ru/kvartiry/360810/', }, }, { coordinates: [47.24542896512635, 56.13448965532694], - label: '3к\n90 м²', + text: '3к\n90 м²', userData: { url: 'https://a101.ru/kvartiry/360810/', }, }, { coordinates: [47.24510451854659, 56.134541185948585], - label: '3к\n77.2 м²', + text: '3к\n77.2 м²', userData: { url: 'https://a101.ru/kvartiry/360810/', }, @@ -168,74 +196,53 @@ export const REALTY_SCENE: BuildingOptions[] = [ zoom: 20, rotation: -130, }, - poiGroups: [ + labelGroups: [ { id: '1111', - type: 'primary', + image: 'default', minZoom: 19.7, elevation: 7, fontSize: 12, fontColor: '#3a3a3a', - data: [ + labels: [ { coordinates: [47.24452417991248, 56.13469284843933], - label: '1к\n27 м²', + text: '1к\n27 м²', userData: { url: 'https://a101.ru/kvartiry/360810/', }, }, { coordinates: [47.24457199258783, 56.13477179423035], - label: '2к\n54.4 м²', + text: '2к\n54.4 м²', userData: { url: 'https://a101.ru/kvartiry/360810/', }, }, { coordinates: [47.244491707517696, 56.13463324895681], - label: '1к\n27 м²', + text: '1к\n27 м²', userData: { url: 'https://a101.ru/kvartiry/360810/', }, }, { coordinates: [47.244467722972786, 56.13455859493207], - label: '3к\n67 м²', + text: '3к\n67 м²', userData: { url: 'https://a101.ru/kvartiry/360810/', }, }, { coordinates: [47.24459718584492, 56.13483803780593], - label: '1к\n30 м²', + text: '1к\n30 м²', userData: { url: 'https://a101.ru/kvartiry/360810/', }, }, { coordinates: [47.24474865936005, 56.13481340001352], - label: '2к\n45 м²', - userData: { - url: 'https://a101.ru/kvartiry/360810/', - }, - }, - { - coordinates: [47.244714550432995, 56.13474141463477], - label: '3к\n54.4 м²', - userData: { - url: 'https://a101.ru/kvartiry/360810/', - }, - }, - { - coordinates: [47.24464159162246, 56.134578465378226], - label: '1к\n33 м²', - userData: { - url: 'https://a101.ru/kvartiry/360810/', - }, - }, - { - coordinates: [47.24461054223749, 56.13451937931448], - label: '2к\n45 м²', + text: '2к\n45 м²', userData: { url: 'https://a101.ru/kvartiry/360810/', }, @@ -254,74 +261,80 @@ export const REALTY_SCENE: BuildingOptions[] = [ zoom: 19.2, rotation: -130, }, - poiGroups: [ + labelGroups: [ { id: '1111', - type: 'primary', + image: { + url: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjRweCIgaGVpZ2h0PSIyNHB4IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGcgaWQ9IlBhZ2UtMSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+PGcgaWQ9ImV4aXQiIGZpbGw9IiM2MDVFNTAiIGZpbGwtcnVsZT0ibm9uemVybyI+PHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiByeD0iNSI+PC9yZWN0PjwvZz48L2c+PC9zdmc+', + size: [38, 38], + stretchX: [[4, 24]], + stretchY: [[4, 24]], + padding: [5, 10, 5, 10], + }, minZoom: 18.9, elevation: 53, fontSize: 12, - fontColor: '#3a3a3a', - data: [ + fontColor: '#fff', + labels: [ { coordinates: [47.24452417991248, 56.13469284843933], - label: '1к\n27 м²', + text: '1к\n27 м²', userData: { url: 'https://a101.ru/kvartiry/360810/', }, }, { coordinates: [47.24457199258783, 56.13477179423035], - label: '2к\n54.4 м²', + text: '2к\n54.4 м²', userData: { url: 'https://a101.ru/kvartiry/360810/', }, }, { coordinates: [47.244491707517696, 56.13463324895681], - label: '1к\n27 м²', + text: '1к\n27 м²', userData: { url: 'https://a101.ru/kvartiry/360810/', }, }, { coordinates: [47.244467722972786, 56.13455859493207], - label: '3к\n67 м²', + text: '3к\n67 м²', userData: { url: 'https://a101.ru/kvartiry/360810/', }, }, { coordinates: [47.24459718584492, 56.13483803780593], - label: '1к\n30 м²', + text: '1к\n30 м²', userData: { url: 'https://a101.ru/kvartiry/360810/', }, }, { coordinates: [47.24474865936005, 56.13481340001352], - label: '2к\n45 м²', + text: '2к\n45 м²', userData: { url: 'https://a101.ru/kvartiry/360810/', }, }, { coordinates: [47.244714550432995, 56.13474141463477], - label: '3к\n54.4 м²', + text: '3к\n54.4 м²', userData: { url: 'https://a101.ru/kvartiry/360810/', }, }, { coordinates: [47.24464159162246, 56.134578465378226], - label: '1к\n33 м²', + text: '1к\n33 м²', userData: { url: 'https://a101.ru/kvartiry/360810/', }, }, { coordinates: [47.24461054223749, 56.13451937931448], - label: '2к\n45 м²', + text: '2к\n45 м²', userData: { url: 'https://a101.ru/kvartiry/360810/', }, diff --git a/package-lock.json b/package-lock.json index e034cf3..5d3a9b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.3.1", "license": "BSD-2-Clause", "devDependencies": { - "@2gis/mapgl": "1.37.2", + "@2gis/mapgl": "1.46.0", "@documentalist/compiler": "^2.8.1", "@types/geojson": "^7946.0.10", "@types/jest": "^27.4.0", @@ -36,9 +36,9 @@ } }, "node_modules/@2gis/mapgl": { - "version": "1.37.2", - "resolved": "https://registry.npmjs.org/@2gis/mapgl/-/mapgl-1.37.2.tgz", - "integrity": "sha512-JrngAj++tHpbnE1BlJCjGXTyZWpZeRR14no8tqRR3sqNmHfo5vWbr1Z0+kcSWpyfUGrlmd+JnG5ppP8f8NeS3A==", + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/@2gis/mapgl/-/mapgl-1.46.0.tgz", + "integrity": "sha512-fHgVnzNXMof3cZ1vBcZMMGcUdjtQNOG5pymJyaeVg01rkHEEvhg8zupcjYlqnCDaATR33mItV/f0/N9bGiMKmg==", "dev": true, "dependencies": { "@types/geojson": "^7946.0.7" @@ -10553,9 +10553,9 @@ }, "dependencies": { "@2gis/mapgl": { - "version": "1.37.2", - "resolved": "https://registry.npmjs.org/@2gis/mapgl/-/mapgl-1.37.2.tgz", - "integrity": "sha512-JrngAj++tHpbnE1BlJCjGXTyZWpZeRR14no8tqRR3sqNmHfo5vWbr1Z0+kcSWpyfUGrlmd+JnG5ppP8f8NeS3A==", + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/@2gis/mapgl/-/mapgl-1.46.0.tgz", + "integrity": "sha512-fHgVnzNXMof3cZ1vBcZMMGcUdjtQNOG5pymJyaeVg01rkHEEvhg8zupcjYlqnCDaATR33mItV/f0/N9bGiMKmg==", "dev": true, "requires": { "@types/geojson": "^7946.0.7" diff --git a/package.json b/package.json index c96d9f7..e7babad 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "author": "2GIS WebMaps Team", "license": "BSD-2-Clause", "devDependencies": { - "@2gis/mapgl": "1.37.2", + "@2gis/mapgl": "1.46.0", "@documentalist/compiler": "^2.8.1", "@types/geojson": "^7946.0.10", "@types/jest": "^27.4.0", diff --git a/src/constants.ts b/src/constants.ts index 21cb673..197fdb1 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,5 +1,7 @@ import type { GeoJsonSourceOptions } from '@2gis/mapgl/types'; +export const PLUGIN_PREFIX = '__mapglPlugins_mapgl-gltf2'; + export const GROUND_COVERING_SOURCE_DATA: GeoJsonSourceOptions['data'] = { type: 'Feature', properties: {}, @@ -17,9 +19,10 @@ export const GROUND_COVERING_SOURCE_DATA: GeoJsonSourceOptions['data'] = { }, }; -export const GROUND_COVERING_SOURCE_PURPOSE = '__mapglPlugins_mapgl-gltf'; +export const GROUND_COVERING_SOURCE_PURPOSE = `${PLUGIN_PREFIX}-covering`; +export const GROUND_COVERING_LAYER_ID = `${PLUGIN_PREFIX}-covering`; export const GROUND_COVERING_LAYER = { - id: '__mapglPlugins_mapgl-gltf', + id: GROUND_COVERING_LAYER_ID, type: 'polygon', style: { color: ['to-color', ['sourceAttr', 'color']], @@ -30,3 +33,5 @@ export const GROUND_COVERING_LAYER = { ['to-boolean', ['sourceAttr', 'color']], ], }; + +export const pluginEvents = ['click', 'mousemove', 'mouseover', 'mouseout'] as const; diff --git a/src/control/index.ts b/src/control/index.ts index 6c2e730..ec964f3 100644 --- a/src/control/index.ts +++ b/src/control/index.ts @@ -5,7 +5,6 @@ import icon_building from 'raw-loader!./icon_building.svg'; import icon_parking from 'raw-loader!./icon_parking.svg'; import classes from './control.module.css'; import { Control } from './control'; -import { Id } from '../types'; const content = /* HTML */ `
@@ -23,7 +22,7 @@ const content = /* HTML */ ` `; /** - * A control for change floor layer level on the plugin. + * A control for change floor layer level in the plugin. * It appears on the map only if you set the `floorControl` option within @type PluginOptions to `true`. * @hidden * @internal @@ -120,7 +119,7 @@ export class GltfFloorControl extends Control { }); } - private _controlHandler = (modelId: Id) => () => { + private _controlHandler = (modelId: string) => () => { this._switchCurrentFloorLevel(modelId); this.emit('floorchange', { @@ -128,7 +127,7 @@ export class GltfFloorControl extends Control { }); }; - private _switchCurrentFloorLevel(modelId: Id) { + private _switchCurrentFloorLevel(modelId: string) { if (this._currentFloorId === undefined) { return; } diff --git a/src/control/types.ts b/src/control/types.ts index 453ce22..a7803c4 100644 --- a/src/control/types.ts +++ b/src/control/types.ts @@ -1,33 +1,31 @@ -import { Id } from '../types/plugin'; - /** - * Floor level data + * Floor level data. */ export interface FloorLevel { - modelId: Id; // id модели этажа или здания + modelId: string; // id модели этажа или здания text: string; icon?: 'parking' | 'building' | string; } /** - * Options for the method show + * Options for the show method. */ export interface ControlShowOptions { - buildingModelId: Id; - activeModelId: Id; + buildingModelId: string; + activeModelId: string; floorLevels?: FloorLevel[]; } /** - * Event that emitted on button presses of the control + * Event that emitted on button presses of the control. */ export interface FloorChangeEvent { - modelId: Id; // id модели этажа или здания + modelId: string; // id модели этажа или здания } export interface ControlEventTable { /** - * Emitted when floor's plan was changed + * Emitted when floor's plan was changed. */ floorchange: FloorChangeEvent; } diff --git a/src/defaultOptions.ts b/src/defaultOptions.ts index e0383e7..4f74b82 100644 --- a/src/defaultOptions.ts +++ b/src/defaultOptions.ts @@ -1,24 +1,14 @@ import type { PluginOptions } from './types/plugin'; export const defaultOptions: Required = { - hoverHighlight: { + hoverOptions: { color: '#ffffff', - intencity: 0.0, }, modelsBaseUrl: '', modelsLoadStrategy: 'waitAll', - poiConfig: { - primary: { - fontSize: 14, - fontColor: '#000000', - }, - secondary: { - fontSize: 14, - fontColor: '#000000', - }, - }, floorsControl: { position: 'centerLeft', }, groundCoveringColor: '#F8F8EBCC', + zIndex: 0, }; diff --git a/src/external/evented.ts b/src/external/evented.ts index e246973..7dd8538 100644 --- a/src/external/evented.ts +++ b/src/external/evented.ts @@ -1,5 +1,5 @@ /** - * Event emitter + * Event emitter. */ export class Evented { private events: { [K in keyof M]?: Array<(ev: M[K]) => void> }; @@ -12,9 +12,10 @@ export class Evented { } /** - * Registers event listener - * @param type Event type - * @param listener Event handler + * Registers event listener. + * + * @param type Event type. + * @param listener Event handler. */ public on(type: K, listener: (ev: M[K]) => void): this { let eventsByType = this.events[type]; @@ -26,9 +27,10 @@ export class Evented { } /** - * Registers event listener which will be called once - * @param type Event type - * @param listener Event handler + * Registers event listener which will be called once. + * + * @param type Event type. + * @param listener Event handler. */ public once(type: K, listener: (ev: M[K]) => void): this { const wrapper = (data: M[K]) => { @@ -42,9 +44,10 @@ export class Evented { } /** - * Removes event listener registered with `on` - * @param type Event type - * @param listener Event handler + * Removes event listener registered with `on`. + * + * @param type Event type. + * @param listener Event handler. */ public off(type: K, listener: (ev: M[K]) => void): this { const eventsByType = this.events[type]; @@ -63,9 +66,10 @@ export class Evented { } /** - * Calls all event listeners with event type `type` - * @param type Event type - * @param data Data transferred to events + * Calls all event listeners with event type `type`. + * + * @param type Event type. + * @param data Data transferred to events. */ public emit(type: K, data?: M[K]): this { const eventsByType = this.events[type]; diff --git a/src/labelGroups.ts b/src/labelGroups.ts new file mode 100644 index 0000000..da8923b --- /dev/null +++ b/src/labelGroups.ts @@ -0,0 +1,73 @@ +import type { Map as MapGL, Label, LabelImage } from '@2gis/mapgl/types'; +import type { BuildingState, LabelGroupOptions, PluginOptions } from './types/plugin'; +import type { GltfPlugin } from './plugin'; +// import { pluginEvents } from './constants'; +// import { createLabelEvenData } from './utils/events'; + +const DEFAULT_IMAGE: LabelImage = { + url: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjgiIGhlaWdodD0iMjgiIHZpZXdCb3g9IjAgMCAyOCAyOCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMjgiIGhlaWdodD0iMjgiIHJ4PSI0IiBmaWxsPSIjZWFlYWVhIi8+PHJlY3QgeD0iMSIgeT0iMSIgd2lkdGg9IjI2IiBoZWlnaHQ9IjI2IiByeD0iMyIgZmlsbD0id2hpdGUiLz48L3N2Zz4=', + size: [38, 38], + stretchX: [[4, 24]], + stretchY: [[4, 24]], + padding: [5, 10, 5, 10], +}; + +export class LabelGroups { + private labelsByGroupId: Map = new Map(); + + constructor( + private map: MapGL, + private plugin: GltfPlugin, + private options: Required, + ) {} + + public add(groupOptions: LabelGroupOptions, state?: BuildingState) { + const { id } = groupOptions; + if (this.labelsByGroupId.has(id)) { + console.error( + `Poi group with id "${id}" already exists. Please use different identifiers for poi groups`, + ); + return; + } + + const { image, minZoom, maxZoom, fontColor: color, fontSize } = groupOptions; + const labels = groupOptions.labels.map((labelOptions) => { + const { coordinates, text, userData } = labelOptions; + const label = new mapgl.Label(this.map, { + coordinates, // + label.elevation ?? groupOptions.elevation + text, + userData, + image: image === 'default' ? DEFAULT_IMAGE : image, + minZoom, + maxZoom, + color, + fontSize, + relativeAnchor: [0.5, 1], + zIndex: this.options.zIndex + 0.00001, // чтобы были выше моделей + }); + + // pluginEvents.forEach((eventType) => { + // label.on(eventType, (ev) => { + // this.plugin.emit(eventType, createLabelEvenData(ev, labelOptions, state)); + // }); + // }); + + return label; + }); + + this.labelsByGroupId.set(id, labels); + } + + public remove(id: string) { + const labels = this.labelsByGroupId.get(id); + this.labelsByGroupId.delete(id); + labels?.forEach((label) => label.destroy()); + } + + public destroy() { + this.labelsByGroupId.forEach((labels) => { + labels.forEach((label) => label.destroy()); + }); + this.labelsByGroupId.clear(); + } +} diff --git a/src/plugin.ts b/src/plugin.ts index bcf96f8..cc4859f 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,7 +1,7 @@ -import type { Map as MapGL } from '@2gis/mapgl/types'; +import type { DynamicObjectEventTable, GltfModel, Map as MapGL } from '@2gis/mapgl/types'; import type { BuildingOptions } from './types/realtyScene'; import type { GltfPluginEventTable } from './types/events'; -import type { Id, PluginOptions, ModelOptions, BuildingState } from './types/plugin'; +import type { PluginOptions, ModelOptions, BuildingState, LabelGroupOptions } from './types/plugin'; import { applyOptionalDefaults } from './utils/common'; import { Evented } from './external/evented'; @@ -9,11 +9,14 @@ import { defaultOptions } from './defaultOptions'; import { concatUrl, isAbsoluteUrl } from './utils/url'; import { createModelEventData } from './utils/events'; import { RealtyScene } from './realtyScene/realtyScene'; -import { GROUND_COVERING_LAYER } from './constants'; +import { ModelStatus } from './types/plugin'; +import { pluginEvents } from './constants'; +import { LabelGroups } from './labelGroups'; interface Model { - instance: any; // GltfModel + instance: GltfModel; options: ModelOptions; + isLoaded: boolean; } const MODEL_DEFAULTS = { @@ -27,17 +30,19 @@ const MODEL_DEFAULTS = { export class GltfPlugin extends Evented { private map: MapGL; private options: Required; - private models: Map; + private models: Map; + private labelGroups: LabelGroups; private realtyScene?: RealtyScene; + private isDestroyed = false; /** - * The main class of the plugin + * The main class of the plugin. * * Example: * ```js - * const plugin = new GltfPlugin (map, { + * const plugin = new GltfPlugin(map, { * modelsLoadStrategy: 'waitAll', - * ambientLight: { color: 'white', intencity: 2.5 }, + * modelsBaseUrl: 'https://url_to_models', * }); * * plugin.addModels([ @@ -50,8 +55,8 @@ export class GltfPlugin extends Evented { * }, * ]); * ``` - * @param map The map instance - * @param pluginOptions GltfPlugin initialization options + * @param map The map instance. + * @param pluginOptions GltfPlugin initialization options. */ constructor(map: MapGL, pluginOptions?: PluginOptions) { super(); @@ -59,15 +64,27 @@ export class GltfPlugin extends Evented { this.map = map; this.options = applyOptionalDefaults(pluginOptions ?? {}, defaultOptions); this.models = new Map(); + this.labelGroups = new LabelGroups(this.map, this, this.options); + } - map.on('styleload', () => { - this.map.addLayer(GROUND_COVERING_LAYER); // мб унести отсюда в RealtyScene, нужно подумать - // this.poiGroups.onMapStyleUpdate(); + /** + * Destroys the plugin. + */ + public destroy() { + this.isDestroyed = true; + this.models.forEach((model) => { + model.instance.destroy(); }); + this.models.clear(); + this.labelGroups.destroy(); + this.realtyScene?.destroy(); } - // public destroy() {} - + /** + * Sets options of the plugin. + * + * @param pluginOptions Plugin options that are available for setting. + */ public setOptions(pluginOptions: Pick, 'groundCoveringColor'>) { Object.keys(pluginOptions).forEach((option) => { switch (option) { @@ -80,11 +97,24 @@ export class GltfPlugin extends Evented { }); } + /** + * Adds a model to the map. + * + * @param modelToLoad Options of a model. + * @param hideOnLoad Set to `true` if a model should be hidden on loading completion. + */ public async addModel(modelToLoad: ModelOptions, hideOnLoad = false) { return this.addModels([modelToLoad], hideOnLoad ? [] : [modelToLoad.modelId]); } - public async addModels(modelsToLoad: ModelOptions[], modelIdsToShow?: Id[]) { + /** + * Adds a list of models to the map. + * + * @param modelsToLoad An array of options of models. + * @param modelIdsToShow An array of ids of models that should be shown. If it's not provided + * all models will be shown. + */ + public async addModels(modelsToLoad: ModelOptions[], modelIdsToShow?: string[]) { const loadingModels = modelsToLoad .filter((options) => { if (this.models.has(options.modelId)) { @@ -121,28 +151,39 @@ export class GltfPlugin extends Evented { hideOnInit: this.options.modelsLoadStrategy === 'waitAll' || (modelIdsToShow && !modelIdsToShow.includes(options.modelId)), + hover: { + color: this.options.hoverOptions.color, + }, + disableAnimation: true, + zIndex: this.options.zIndex, }); - const model = { + const model: Model = { options, instance, + isLoaded: false, }; this.models.set(options.modelId, model); return new Promise((resolve) => { - instance.once('modelloaded', () => resolve(model)); - (['click', 'mousemove', 'mouseover', 'mouseout'] as const).forEach( - (eventType) => { - instance.on(eventType, (ev) => { - this.emit(eventType, createModelEventData(ev, options)); - }); - }, - ); + instance.once('modelloaded' as keyof DynamicObjectEventTable, () => { + model.isLoaded = true; + resolve(model); + }); + pluginEvents.forEach((eventType) => { + instance.on(eventType, (ev) => { + this.emit(eventType, createModelEventData(ev, options)); + }); + }); }); }); return Promise.all(loadingModels).then((loadedModels) => { + if (this.isDestroyed) { + return; + } + if (this.options.modelsLoadStrategy !== 'waitAll') { return; } @@ -155,11 +196,27 @@ export class GltfPlugin extends Evented { }); } - public isModelAdded(id: Id) { - return this.models.has(id); + /** + * Returns a current status of a model. + * There can be no model or it can be loading or loaded. + * + * @param id A model id. + */ + public getModelStatus(id: string) { + const model = this.models.get(id); + if (!model) { + return ModelStatus.NoModel; + } + + return !model.isLoaded ? ModelStatus.Loading : ModelStatus.Loaded; } - public removeModel(id: Id) { + /** + * Removes a model from the map. + * + * @param id A model id. + */ + public removeModel(id: string) { const model = this.models.get(id); if (model) { model.instance.destroy(); @@ -167,35 +224,84 @@ export class GltfPlugin extends Evented { } } - public removeModels(ids: Id[]) { + /** + * Removes models from the map. + * + * @param id Model ids. + */ + public removeModels(ids: string[]) { ids.forEach((id) => this.removeModel(id)); } - public showModel(id: Id) { + /** + * Shows a model on the map. + * + * @param id A model id. + */ + public showModel(id: string) { this.models.get(id)?.instance.show(); } - public showModels(ids: Id[]) { + /** + * Shows models on the map. + * + * @param id Model ids. + */ + public showModels(ids: string[]) { ids.forEach((id) => this.showModel(id)); } - public hideModel(id: Id) { + /** + * Hides a model on the map. + * + * @param id A model id. + */ + public hideModel(id: string) { this.models.get(id)?.instance.hide(); } - public hideModels(ids: Id[]) { + /** + * Hides models on the map. + * + * @param id Model ids. + */ + public hideModels(ids: string[]) { ids.forEach((id) => this.hideModel(id)); } + /** + * Adds a group of labels to the map. + * + * @param options Options of the group of labels. + * @param state A state of active building and floor a group of labels is associated with. + */ + public addLabelGroup(options: LabelGroupOptions, state?: BuildingState) { + this.labelGroups.add(options, state); + } + + /** + * Removes a group of labels from the map. + * + * @param id A label group id. + */ + public removeLabelGroup(id: string) { + this.labelGroups.remove(id); + } + + /** + * Adds an interactive realty scene to the map. + * + * @param scene Options of the scene to add to the map. + * @param state A state of building and floor that should be active on realty scene initialization. + */ public async addRealtyScene(scene: BuildingOptions[], state?: BuildingState) { this.realtyScene = new RealtyScene(this, this.map, this.options); return this.realtyScene.init(scene, state); } - // public showRealtyScene() {} - - // public hideRealtyScene() {} - + /** + * Removes an interactive realty scene from the map. + */ public removeRealtyScene() { this.realtyScene?.destroy(); this.realtyScene = undefined; diff --git a/src/poiGroups.ts b/src/poiGroups.ts deleted file mode 100644 index 0f69cb5..0000000 --- a/src/poiGroups.ts +++ /dev/null @@ -1,155 +0,0 @@ -import type { FeatureCollection, Feature, Point } from 'geojson'; -import type { Map as MapGL, GeoJsonSource } from '@2gis/mapgl/types'; - -import type { Id, PluginOptions, BuildingState, PoiGroupOptions, PoiOptions } from './types/plugin'; -import type { PoiGeoJsonProperties } from './types/events'; - -type FeaturePoint = Feature; - -export class PoiGroups { - private poiSources = new Map(); - private activePoiGroupOptions?: PoiGroupOptions; - - constructor(private map: MapGL, private poiConfig: PluginOptions['poiConfig']) {} - - public onMapStyleUpdate() { - this.map.addIcon('km_pillar_gray_border', { - url: 'https://disk.2gis.com/styles/d7e8aed1-4d3f-472a-a1e4-337f4b31ab8a/km_pillar_gray_border', - // @ts-ignore - width: 38, - height: 38, - stretchX: [[4, 24]], - stretchY: [[4, 24]], - }); - - if (this.activePoiGroupOptions) { - this.addPoiStyleLayer(this.activePoiGroupOptions); - } - } - - public async add(groupOptions: PoiGroupOptions, state?: BuildingState) { - this.activePoiGroupOptions = groupOptions; - const { id, data } = groupOptions; - const actualId = String(id); - if (this.poiSources.get(actualId) !== undefined) { - throw new Error( - `Poi group with id "${actualId}" already exists. Please use different identifiers for poi groups`, - ); - } - - const geoJson = this.createGeoJson(data, groupOptions, state); - - // create source with poi - const source = new mapgl.GeoJsonSource(this.map, { - data: geoJson, - attributes: { - dataType: actualId, - }, - }); - this.poiSources.set(actualId, source); - - // add style layer for poi - this.addPoiStyleLayer(groupOptions); - } - - public remove(origId: Id) { - this.activePoiGroupOptions = undefined; - const id = String(origId); - const source = this.poiSources.get(id); - this.poiSources.delete(id); - source?.destroy(); - this.map.removeLayer('plugin-poi-' + id); - } - - private createGeoJson( - poiOptions: PoiOptions[], - groupOptions: PoiGroupOptions, - state?: BuildingState, - ): FeatureCollection { - const { elevation } = groupOptions; - const features: FeaturePoint[] = poiOptions.map((opts) => ({ - type: 'Feature', - properties: { - // main properties - type: 'immersive_poi', - label: opts.label, - userData: opts.userData, - elevation: elevation, - coordinates: opts.coordinates, - // auxilary properties - modelId: state?.modelId, - floorId: state?.floorId, - }, - geometry: { - type: 'Point', - coordinates: opts.coordinates, - }, - })); - - return { - type: 'FeatureCollection', - features, - }; - } - - private addPoiStyleLayer(groupOptions: PoiGroupOptions) { - const { id, type, minZoom = -Infinity, maxZoom = +Infinity } = groupOptions; - let { fontSize, fontColor } = groupOptions; - const actualId = String(id); - let style; - - if (fontColor === undefined) { - fontColor = - type === 'primary' - ? this.poiConfig?.primary?.fontColor ?? '#3a3a3a' - : this.poiConfig?.secondary?.fontColor ?? '#3a3a3a'; - } - if (fontSize === undefined) { - fontSize = - type === 'primary' - ? this.poiConfig?.primary?.fontSize ?? 14 - : this.poiConfig?.secondary?.fontSize ?? 12; - } - - if (type === 'primary') { - style = { - iconPriority: 7000, - allowElevation: true, - elevation: ['get', 'elevation'], - iconImage: 'km_pillar_gray_border', - iconAnchor: [0.5, 1], - iconOffset: [0, 0], - iconTextFont: 'Noto_Sans', - iconTextColor: fontColor, - iconTextField: ['get', 'label'], - iconTextPadding: [5, 10, 5, 10], - iconTextFontSize: fontSize, - duplicationSpacing: 1, - }; - } else { - style = { - allowElevation: true, - elevation: ['get', 'elevation'], - duplicationSpacing: 1, - textField: ['get', 'label'], - textFont: 'Noto_Sans', - textFontSize: fontSize, - textColor: fontColor, - textPriority: 6000, - }; - } - - this.map.addLayer({ - type: 'point', - id: 'plugin-poi-' + actualId, - filter: [ - 'all', - ['match', ['sourceAttr', 'dataType'], [actualId], true, false], - ['match', ['get', 'type'], ['immersive_poi'], true, false], - ], - style, - minzoom: minZoom, - maxzoom: maxZoom, - }); - } -} diff --git a/src/realtyScene/realtyScene.ts b/src/realtyScene/realtyScene.ts index f21f62f..169cadc 100644 --- a/src/realtyScene/realtyScene.ts +++ b/src/realtyScene/realtyScene.ts @@ -4,46 +4,50 @@ import { GltfPlugin } from '../plugin'; import { GltfFloorControl } from '../control'; import classes from './realtyScene.module.css'; -import type { BuildingState, Id, ModelOptions, PluginOptions } from '../types/plugin'; +import { + ModelStatus, + type BuildingState, + type ModelOptions, + type PluginOptions, +} from '../types/plugin'; import type { BuildingOptions, MapOptions, - BuildingFloorOptions, PopupOptions, + BuildingOptionsInternal, + BuildingFloorOptionsInternal, + RealtySceneState, } from '../types/realtyScene'; -import type { FloorLevel, FloorChangeEvent } from '../control/types'; -import type { GltfPluginModelEvent, GltfPluginPoiEvent } from '../types/events'; -import { GROUND_COVERING_SOURCE_DATA, GROUND_COVERING_SOURCE_PURPOSE } from '../constants'; - -interface RealtySceneState { - activeModelId?: Id; - - // id здания мапится на опции здания или опции этажа этого здания - buildingVisibility: Map; -} - -type BuildingOptionsInternal = Omit & { - floors: FloorLevel[]; -}; -type BuildingFloorOptionsInternal = BuildingFloorOptions & { - buildingOptions: ModelOptions; -}; +import type { FloorChangeEvent } from '../control/types'; +import type { GltfPluginModelEvent, GltfPluginLabelEvent } from '../types/events'; +import { + GROUND_COVERING_SOURCE_DATA, + GROUND_COVERING_SOURCE_PURPOSE, + GROUND_COVERING_LAYER, + GROUND_COVERING_LAYER_ID, +} from '../constants'; +import { + getBuildingModelOptions, + getFloorModelId, + getFloorModelOptions, + getFloorPoiGroupId, + isObject, +} from '../utils/realtyScene'; export class RealtyScene { - private buildings = new Map(); - private floors = new Map(); - private undergroundFloors = new Set(); + private buildings = new Map(); + private floors = new Map(); + private undergroundFloors = new Set(); private state: RealtySceneState = { activeModelId: undefined, buildingVisibility: new Map(), }; + private isDestroyed = false; private groundCoveringSource: GeoJsonSource; private control: GltfFloorControl; private popup?: HtmlMarker; - // private poiGroups: PoiGroups; - constructor( private plugin: GltfPlugin, private map: MapGL, @@ -51,7 +55,6 @@ export class RealtyScene { ) { const { position } = this.options.floorsControl; this.control = new GltfFloorControl(this.map, { position }); - // this.poiGroups = new PoiGroups(this.map, this.options.poiConfig); this.groundCoveringSource = new mapgl.GeoJsonSource(map, { maxZoom: 2, data: GROUND_COVERING_SOURCE_DATA, @@ -59,9 +62,12 @@ export class RealtyScene { purpose: GROUND_COVERING_SOURCE_PURPOSE, }, }); + + this.map.addLayer(GROUND_COVERING_LAYER); + map.on('styleload', this.onStyleLoad); } - private getBuildingModelId(id: Id | undefined) { + private getBuildingModelId(id: string | undefined) { if (id === undefined) { return; } @@ -79,44 +85,109 @@ export class RealtyScene { private setState(newState: RealtySceneState) { const prevState = this.state; + // т.к. стейт может меняться асинхронно и иногда нужно показывать + // предыдущую модель некоторое время, реальный стейт заполняется тут, + // а выставление нужного будет отложено на время загрузки модели + const buildingVisibility: Map = new Map(); + this.buildings.forEach((_, buildingId) => { const prevModelOptions = prevState.buildingVisibility.get(buildingId); const newModelOptions = newState.buildingVisibility.get(buildingId); - if (prevModelOptions) { - this.plugin.hideModel(prevModelOptions.modelId); - } - - if (newModelOptions) { - this.plugin.isModelAdded(newModelOptions.modelId) - ? this.plugin.showModel(newModelOptions.modelId) - : this.plugin.addModel(newModelOptions); - } - }); - if (prevState.activeModelId !== newState.activeModelId) { - if ( - prevState.activeModelId !== undefined && - this.undergroundFloors.has(prevState.activeModelId) - ) { - this.switchOffGroundCovering(); + // если опции не изменились, то ничего не делаем + if (prevModelOptions?.modelId === newModelOptions?.modelId) { + buildingVisibility.set(buildingId, prevModelOptions); + return; } - if (newState.activeModelId !== undefined) { - const options = - this.buildings.get(newState.activeModelId) ?? - this.floors.get(newState.activeModelId); - if (options) { - this.setMapOptions(options.mapOptions); - // this.addFloorPoi(activeFloor); - // this.clearPoiGroups(); + if (prevModelOptions) { + // если нужно отобразить подземный этаж, но его модель не готова, то ничего не скрываем + if ( + !newModelOptions && + newState.activeModelId !== undefined && + this.undergroundFloors.has(newState.activeModelId) && + this.plugin.getModelStatus(newState.activeModelId) !== ModelStatus.Loaded + ) { + buildingVisibility.set(buildingId, prevModelOptions); + } else if ( + // если новая модель готова или предыдущую нужно просто скрыть, то скрываем ее + !newModelOptions || + this.plugin.getModelStatus(newModelOptions.modelId) === ModelStatus.Loaded + ) { + this.plugin.hideModel(prevModelOptions.modelId); + buildingVisibility.set(buildingId, undefined); + + if (this.undergroundFloors.has(prevModelOptions.modelId)) { + this.switchOffGroundCovering(); + } + + const floorOptions = this.floors.get(prevModelOptions.modelId); + if (floorOptions) { + floorOptions.labelGroups?.forEach((group) => { + this.plugin.removeLabelGroup(group.id); + }); + } } + } - if (this.undergroundFloors.has(newState.activeModelId)) { - this.switchOnGroundCovering(); + if (newModelOptions) { + const modelStatus = this.plugin.getModelStatus(newModelOptions.modelId); + + // если новая модель готова, то показываем ее + if (modelStatus === ModelStatus.Loaded) { + this.plugin.showModel(newModelOptions.modelId); + buildingVisibility.set(buildingId, newModelOptions); + + // если модель активна, то применяем опции карты и включаем подложку, если нужно + if ( + newState.activeModelId !== undefined && + newState.activeModelId === newModelOptions.modelId + ) { + const options = + this.buildings.get(newModelOptions.modelId) ?? + this.floors.get(newModelOptions.modelId); + + if (options) { + this.setMapOptions(options.mapOptions); + } + + if (this.undergroundFloors.has(newModelOptions.modelId)) { + this.switchOnGroundCovering(); + } + + const floorOptions = this.floors.get(newModelOptions.modelId); + if (floorOptions) { + floorOptions.labelGroups?.forEach((group) => { + this.plugin.addLabelGroup(group, { + buildingId, + floorId: floorOptions.id, + }); + }); + } + } + } else { + if (modelStatus === ModelStatus.NoModel) { + this.plugin.addModel(newModelOptions, true).then(() => { + if (this.isDestroyed) { + return; + } + + if (this.state.activeModelId !== newModelOptions.modelId) { + return; + } + + // откладываем выставление нужного стейта до момента загрузки модели + this.setState(newState); + }); + } + + // если новые модели не готовы, то пока показываем предыдущие + buildingVisibility.set(buildingId, prevModelOptions); } } - } + }); + // контрол реагирует на изменения стейта сразу, без учета загрузки модели, т.к. завязан на здание в целом const prevBuildingModelId = this.getBuildingModelId(prevState.activeModelId); const newBuildingModelId = this.getBuildingModelId(newState.activeModelId); @@ -140,12 +211,15 @@ export class RealtyScene { } } - this.state = newState; + this.state = { + buildingVisibility, + activeModelId: newState.activeModelId, + }; } public async init(scene: BuildingOptions[], state?: BuildingState) { // Приводим стейт пользователя к внутреннему виду id - let activeModelId: Id | undefined = state + let activeModelId: string | undefined = state ? state.floorId ? getFloorModelId(state.buildingId, state.floorId) : state.buildingId @@ -169,6 +243,10 @@ export class RealtyScene { this.floors.set(floorModelId, { ...floor, + labelGroups: (floor.labelGroups ?? []).map((group) => ({ + ...group, + id: getFloorPoiGroupId(building.modelId, floor.id, group.id), + })), buildingOptions: buildingOptions, }); @@ -187,8 +265,8 @@ export class RealtyScene { ? activeModelId : undefined; - const modelsToLoad: Map = new Map(); - const buildingVisibility: Map = new Map(); + const modelsToLoad: Map = new Map(); + const buildingVisibility: Map = new Map(); this.buildings.forEach((options, id) => { const modelOptions = getBuildingModelOptions(options); @@ -198,6 +276,7 @@ export class RealtyScene { if (activeModelId) { const floorOptions = this.floors.get(activeModelId); + if (floorOptions) { if (this.undergroundFloors.has(activeModelId)) { buildingVisibility.clear(); // показываем только подземный этаж @@ -218,6 +297,10 @@ export class RealtyScene { return this.plugin .addModels(Array.from(modelsToLoad.values()), Array.from(buildingVisibility.keys())) .then(() => { + if (this.isDestroyed) { + return; + } + this.setState({ activeModelId, buildingVisibility, @@ -241,14 +324,20 @@ export class RealtyScene { } public destroy() { + this.isDestroyed = true; + this.map.off('styleload', this.onStyleLoad); this.plugin.off('click', this.onSceneClick); this.plugin.off('mouseover', this.onSceneMouseOver); this.plugin.off('mouseout', this.onSceneMouseOut); this.control.off('floorchange', this.floorChangeHandler); + this.floors.forEach(({ labelGroups }) => { + labelGroups?.forEach(({ id }) => { + this.plugin.removeLabelGroup(id); + }); + }); this.plugin.removeModels([...this.buildings.keys(), ...this.floors.keys()]); - - // this.clearPoiGroups(); + this.map.removeLayer(GROUND_COVERING_LAYER_ID); this.groundCoveringSource.destroy(); this.undergroundFloors.clear(); @@ -288,7 +377,11 @@ export class RealtyScene { } } - private onSceneMouseOut = (ev: GltfPluginPoiEvent | GltfPluginModelEvent) => { + private onStyleLoad = () => { + this.map.addLayer(GROUND_COVERING_LAYER); + }; + + private onSceneMouseOut = (ev: GltfPluginLabelEvent | GltfPluginModelEvent) => { if (ev.target.type !== 'model') { return; } @@ -296,8 +389,8 @@ export class RealtyScene { this.popup?.destroy(); }; - private onSceneMouseOver = ({ target }: GltfPluginPoiEvent | GltfPluginModelEvent) => { - if (target.type === 'poi' || target.modelId === undefined) { + private onSceneMouseOver = ({ target }: GltfPluginLabelEvent | GltfPluginModelEvent) => { + if (target.type === 'label' || target.modelId === undefined) { return; } @@ -313,13 +406,13 @@ export class RealtyScene { }); }; - private onSceneClick = ({ target }: GltfPluginPoiEvent | GltfPluginModelEvent) => { + private onSceneClick = ({ target }: GltfPluginLabelEvent | GltfPluginModelEvent) => { if (target.type === 'model') { const options = this.buildings.get(target.modelId); if (options) { this.buildingClickHandler(target.modelId); } - } else if (target.type === 'poi') { + } else if (target.type === 'label') { const userData = target.data.userData; if (isObject(userData) && typeof userData.url === 'string') { const a = document.createElement('a'); @@ -331,7 +424,7 @@ export class RealtyScene { }; private floorChangeHandler = (ev: FloorChangeEvent) => { - const buildingVisibility: Map = new Map(); + const buildingVisibility: Map = new Map(); this.buildings.forEach((options, id) => { buildingVisibility.set(id, getBuildingModelOptions(options)); }); @@ -361,14 +454,14 @@ export class RealtyScene { } }; - private buildingClickHandler = (modelId: Id) => { + private buildingClickHandler = (modelId: string) => { const buildingOptions = this.buildings.get(modelId); if (!buildingOptions) { return; } let activeModelId = modelId; - const buildingVisibility: Map = new Map(); + const buildingVisibility: Map = new Map(); this.buildings.forEach((options, id) => { buildingVisibility.set(id, getBuildingModelOptions(options)); }); @@ -396,55 +489,6 @@ export class RealtyScene { }); }; - // private addFloorPoi(floorOptions?: BuildingFloorOptions) { - // if (floorOptions === undefined) { - // return; - // } - - // this.activeModelId = floorOptions.id; - - // this.setMapOptions(floorOptions?.mapOptions); - - // this.clearPoiGroups(); - - // floorOptions.poiGroups?.forEach((poiGroup) => { - // if (this.activeBuilding?.modelId) { - // this.plugin.addPoiGroup(poiGroup, { - // modelId: this.activeBuilding?.modelId, - // floorId: floorOptions.id, - // }); - // this.activePoiGroupIds.push(poiGroup.id); - // } - // }); - // } - - // private clearPoiGroups() { - // this.activePoiGroupIds.forEach((id) => { - // this.plugin.removePoiGroup(id); - // }); - - // this.activePoiGroupIds = []; - // } - - // /** - // * Add the group of poi to the map - // * - // * @param options Options of the group of poi to add to the map - // * @param state State of the active building to connect with added the group of poi - // */ - // public async addPoiGroup(options: PoiGroupOptions, state?: BuildingState) { - // this.poiGroups.add(options, state); - // } - - // /** - // * Remove the group of poi from the map - // * - // * @param id Identifier of the group of poi to remove - // */ - // public removePoiGroup(id: Id) { - // this.poiGroups.remove(id); - // } - private switchOffGroundCovering() { const attrs = { ...this.groundCoveringSource.getAttributes() }; delete attrs['color']; @@ -459,54 +503,8 @@ export class RealtyScene { } } -function getBuildingModelOptions(building: BuildingOptionsInternal): ModelOptions { - return { - modelId: building.modelId, - coordinates: building.coordinates, - modelUrl: building.modelUrl, - rotateX: building.rotateX, - rotateY: building.rotateY, - rotateZ: building.rotateZ, - offsetX: building.offsetX, - offsetY: building.offsetY, - offsetZ: building.offsetZ, - scale: building.scale, - linkedIds: building.linkedIds, - interactive: building.interactive, - }; -} - -function getFloorModelOptions({ - buildingOptions, - id, - modelUrl, -}: BuildingFloorOptionsInternal): ModelOptions { - return { - modelId: getFloorModelId(buildingOptions.modelId, id), - coordinates: buildingOptions.coordinates, - modelUrl, - rotateX: buildingOptions.rotateX, - rotateY: buildingOptions.rotateY, - rotateZ: buildingOptions.rotateZ, - offsetX: buildingOptions.offsetX, - offsetY: buildingOptions.offsetY, - offsetZ: buildingOptions.offsetZ, - scale: buildingOptions.scale, - linkedIds: buildingOptions.linkedIds, - interactive: buildingOptions.interactive, - }; -} - -function getFloorModelId(buildingModelId: string, floorId: string) { - return `${buildingModelId}_${floorId}`; -} - const getPopupHtml = ({ description, title }: PopupOptions) => `

${title}

${description ? `

${description}

` : ''}
`; - -function isObject(value: unknown): value is Record { - return typeof value === 'object' && value !== null && !Array.isArray(value); -} diff --git a/src/types/events.ts b/src/types/events.ts index 6db4834..4c3aa71 100644 --- a/src/types/events.ts +++ b/src/types/events.ts @@ -1,119 +1,102 @@ -import type { Id, ModelOptions, PoiOptions } from './plugin'; - -export type PoiGeoJsonProperties = PoiOptions & { - /** - * Identifier of the building's model - */ - modelId?: Id; - - /** - * Identifier of the floor's model - */ - floorId?: Id; - - /** - * Type of the poi - */ - type?: string; -}; +import type { ModelOptions, LabelOptions } from './plugin'; export interface ModelTarget { /** - * Type of the target + * The type of a target. */ type: 'model'; /** - * The targeted model + * A targeted model. */ data: ModelOptions; /** - * Identifier of the building's or floor's model + * An identifier of the building's or floor's model. */ - modelId: Id; + modelId: string; } -export interface PoiTarget { +export interface LabelTarget { /** - * Type of the target + * The type of a target. */ - type: 'poi'; + type: 'label'; /** - * The targeted poi + * A targeted label. */ - data: PoiGeoJsonProperties; + data: LabelOptions; /** - * Identifier of the building's model + * An identifier of the building's model. */ - modelId?: Id; + buildingId?: string; /** - * Identifier of the current floor + * An identifier of the current floor. */ - floorId?: Id; + floorId?: string; } /** - * The event type for pointer-related plugin events + * Event type for pointer-related plugin events. */ interface GltfPluginPointerEvent { /** - * The original DOM event + * An original DOM event. */ originalEvent: MouseEvent | TouchEvent; /** - * Geographical coordinates of the event + * Geographical coordinates of an event. */ lngLat: number[]; /** - * Screen coordinates of the event + * Screen coordinates of an event. */ point: number[]; } /** - * The event type for pointer-related plugin events emitted by the poi + * Event type for pointer-related plugin events emitted by a label. */ -export interface GltfPluginPoiEvent extends GltfPluginPointerEvent { +export interface GltfPluginLabelEvent extends GltfPluginPointerEvent { /** - * Target of the poi event + * A target of a label event. */ - target: PoiTarget; + target: LabelTarget; } /** - * The event type for pointer-related plugin events emitted by the model + * Event type for pointer-related plugin events emitted by a model. */ export interface GltfPluginModelEvent extends GltfPluginPointerEvent { /** - * Target of the model event + * A target of a model event. */ target: ModelTarget; } /** - * The list of events that can be emitted by the glTF plugin instance + * List of events that can be emitted by the GLTF plugin instance. */ export interface GltfPluginEventTable { /** - * Emitted when model or poi are clicked + * Emitted when a model or a label is clicked. */ - click: GltfPluginPoiEvent | GltfPluginModelEvent; + click: GltfPluginLabelEvent | GltfPluginModelEvent; /** - * Emitted when the user moves the pointer over the model or the poi + * Emitted when user moves pointer over a model or a label. */ - mousemove: GltfPluginPoiEvent | GltfPluginModelEvent; + mousemove: GltfPluginLabelEvent | GltfPluginModelEvent; /** - * Emitted when the user hovers over the model or the poi + * Emitted when user hovers over a model or a label. */ - mouseover: GltfPluginPoiEvent | GltfPluginModelEvent; + mouseover: GltfPluginLabelEvent | GltfPluginModelEvent; /** - * Emitted when the user moves the mouse away from the model or the poi + * Emitted when user moves mouse away from a model or a label. */ - mouseout: GltfPluginPoiEvent | GltfPluginModelEvent; + mouseout: GltfPluginLabelEvent | GltfPluginModelEvent; } diff --git a/src/types/plugin.ts b/src/types/plugin.ts index 9dacf36..9f45ce7 100644 --- a/src/types/plugin.ts +++ b/src/types/plugin.ts @@ -1,26 +1,4 @@ -export type Id = string; - -export type ColorModelString = `${'rgb' | 'hsl'}(${string})`; -export type HexColorString = `#${string}`; - -/** - * Color representation can be rgb(), hsl(), or hex value - */ -export type ColorRepresentation = ColorModelString | HexColorString | number; - -/** - * Configuration of the poi - */ -export interface PoiConfigGranular { - /** - * Size of the font - */ - fontSize?: number; - /** - * Color of the font - */ - fontColor?: string; -} +import type { LabelImage } from '@2gis/mapgl/types'; /** * Possible positions of the control. @@ -40,198 +18,194 @@ export type ControlPosition = */ export interface ControlOptions { /** - * Position of the control. + * A position of the control. */ position: ControlPosition; } /** - * Options for the highlight color of hovered models + * Options for the hover state of models. */ -export interface HightlightOptions { - // TODO: MAJOR. Rename to «HighlightOptions» in the next major release. - /** - * Color of the hover - * @default '#ffffff' - */ - color?: ColorRepresentation; +export interface HoverOptions { /** - * Intensity of the color on the hover in the range from 0 to 1 - * @default 0.0 + * A hover color. */ - intencity: number; // TODO: MAJOR. Rename to «intensity» in the next major release. + color: string; } /** - * Options for the plugin + * Options for the plugin. */ export interface PluginOptions { /** - * The url which is used for resolving of a model's relative url + * A URL which is used for resolving of a model's relative path. */ modelsBaseUrl?: string; /** - * Strategies for the loading of models: - * - dontWaitAll - show models as soon as possible - * - waitAll - show models only when all models are ready for the rendering + * Strategies for loading of models: + * - dontWaitAll - show models as soon as possible. + * - waitAll - show models only when all models are ready for rendering. */ modelsLoadStrategy?: 'dontWaitAll' | 'waitAll'; /** - * Configuration of poi - */ - poiConfig?: { - /** - * Configuration the primary poi - */ - primary?: PoiConfigGranular; - /** - * Configuration the secondary poi - */ - secondary?: PoiConfigGranular; - }; - /** - * Settings for floors' control + * Settings for floors' control. */ floorsControl?: ControlOptions; /** - * Settings of the highlighted models + * Settings of hovered models. */ - hoverHighlight?: HightlightOptions; + hoverOptions?: HoverOptions; /** * Color for the ground covering when an underground floor's plan is shown. */ groundCoveringColor?: string; + /** + * Draw order of plugin objects (models and labels). + * It may be useful when other map objects (such as markers, shapes, etc.) need to be added + * on the map so that user could manage draw order of the plugin and these objects. + */ + zIndex?: number; } /** - * State for the building's scene + * State for the building's scene. */ export interface BuildingState { /** - * Identifier of the building's model + * An identifier of the building's model. */ buildingId: string; /** - * Identifier of the floor's model + * An identifier of the floor's model. */ floorId?: string; } /** - * Options for a model + * Options for a model. */ export interface ModelOptions { /** - * Identifier of the model should be unique for every model + * An identifier of a model should be unique for every model. */ - modelId: Id; + modelId: string; /** - * Geographical coordinates [longitude, latitude] + * Geographical coordinates [longitude, latitude]. */ coordinates: number[]; /** - * Url where the model is located + * URL where a model is located. */ modelUrl: string; /** - * Rotation of the model in degrees about the X axis + * Rotation of a model in degrees about the X axis. */ rotateX?: number; /** - * Rotation of the model in degrees about the Y axis + * Rotation of a model in degrees about the Y axis. */ rotateY?: number; /** - * Rotation of the model in degrees about the Z axis + * Rotation of a model in degrees about the Z axis. */ rotateZ?: number; /** - * Offset of the model along the X axis in meters + * Offset of a model along the X axis in meters. */ offsetX?: number; /** - * Offset of the model along the Y axis in meters + * Offset of a model along the Y axis in meters. */ offsetY?: number; /** - * Offset of the model along the Z axis in meters + * Offset of a model along the Z axis in meters. */ offsetZ?: number; /** - * Scale of the model + * Scale of a model. */ scale?: number; /** - * List of buildings' identifiers that should be hidden + * A list of buildings' identifiers that should be hidden. */ linkedIds?: string[]; /** - * User specific data + * User specific data. */ userData?: any; /** - * Interactivity of model. All models are interactive by default + * Interactivity of model. All models are interactive by default. */ interactive?: boolean; } /** - * Options for a poi + * Options for a label. */ -export interface PoiOptions { +export interface LabelOptions { /** - * Coordinate of the poi + * Coordinates of a label. */ coordinates: [number, number]; /** - * Elevation of the poi + * An elevation of a label. */ elevation?: number; /** - * Elevation of the poi + * A text of a label. */ - label: string; + text: string; /** - * User specific data + * User specific data. */ userData?: any; } /** - * Options for a poi group + * Options for a label group. */ -export interface PoiGroupOptions { +export interface LabelGroupOptions { /** - * Identifier of the poi group to add + * An identifier of a label group to add. */ - id: Id; + id: string; /** - * Type of the poi + * An elevation of a label group. */ - type: 'primary' | 'secondary'; + elevation: number; /** - * Elevation of the group of poi + * An array of labels to add on the map */ - elevation: number; + labels: LabelOptions[]; /** - * Array of poi to add on the map + * Image settings for labels' text background. */ - data: PoiOptions[]; + image?: LabelImage | 'default'; /** - * Minimum display styleZoom of the poi group + * A minimum display styleZoom of a label group. */ minZoom?: number; /** - * Maximum display styleZoom of the poi group + * A maximum display styleZoom of a label group. */ maxZoom?: number; /** - * Size of the poi's font + * A size of a label's font. */ fontSize?: number; /** - * Color of the poi's font + * A color of a label's font. */ fontColor?: string; } + +/** + * Status of a model. + * There can be no model or it can be loading or loaded. + */ +export enum ModelStatus { + NoModel, + Loading, + Loaded, +} diff --git a/src/types/realtyScene.ts b/src/types/realtyScene.ts index e566032..60c52fd 100644 --- a/src/types/realtyScene.ts +++ b/src/types/realtyScene.ts @@ -1,53 +1,54 @@ -import type { Id, PoiGroupOptions, ModelOptions } from './plugin'; +import { FloorLevel } from '../control/types'; +import type { ModelOptions, LabelGroupOptions } from './plugin'; /** - * Options for the map + * Options for the map. */ export interface MapOptions { /** - * Geographical center of the map + * Geographical center of the map. */ center?: number[]; /** - * Map's pitch angle in degrees + * Map's pitch angle in degrees. */ pitch?: number; /** - * Map's rotation angle in degrees + * Map's rotation angle in degrees. */ rotation?: number; /** - * Map's zoom + * Map's zoom. */ zoom?: number; } /** - * Options for a floor's plan on the realty scene + * Options for a floor's plan in the realty scene. */ export interface BuildingFloorOptions { /** - * Identifier of the floor's plan + * An identifier of the floor's plan. */ - id: Id; + id: string; /** - * Text to add to the floors' control + * A text to add to the floors' control. */ text: string; /** - * Url of a model that represents the current floor's plan + * A URL of a model that represents the current floor's plan. */ modelUrl: string; /** - * Icon to add to the floors' control + * An icon to add to the floors' control. */ icon?: 'building' | 'parking' | string; /** - * List of poi groups connected with the floor's plan + * A list of groups of labels connected with the floor's plan. */ - poiGroups?: PoiGroupOptions[]; + labelGroups?: LabelGroupOptions[]; /** - * Map's options to apply after selecting the particular floor + * Map's options to apply after selecting the particular floor. */ mapOptions?: MapOptions; /** @@ -59,37 +60,64 @@ export interface BuildingFloorOptions { } /** - * Options of popup that appears on hover of buildings + * Options of popup that appears on hover of buildings. */ export interface PopupOptions { /** - * Popup's coordinates + * Popup's coordinates. */ coordinates: number[]; /** - * Popup's title + * A popup's title. */ title: string; /** - * Popup's description + * A popup's description. */ description?: string; } /** - * Options for a building on the realty scene + * Options for a building in the realty scene. */ export interface BuildingOptions extends ModelOptions { /** - * Map's options to apply after selecting the particular building + * Map's options to apply after selecting the particular building. */ mapOptions?: MapOptions; /** - * List of the floors' plans connected with the particular building + * A list of the floors' plans connected with the particular building. */ floors?: BuildingFloorOptions[]; /** - * Popup options + * Popup options. */ popupOptions?: PopupOptions; } + +/** + * @hidden + * @internal + */ +export interface RealtySceneState { + activeModelId?: string; + + // id здания мапится на опции здания или опции этажа этого здания + buildingVisibility: Map; +} + +/** + * @hidden + * @internal + */ +export type BuildingOptionsInternal = Omit & { + floors: FloorLevel[]; +}; + +/** + * @hidden + * @internal + */ +export type BuildingFloorOptionsInternal = BuildingFloorOptions & { + buildingOptions: ModelOptions; +}; diff --git a/src/utils/common.ts b/src/utils/common.ts index c9f3f74..5fd50dd 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -1,11 +1,3 @@ -export function clamp(value: number, min: number, max: number): number { - value = Math.max(value, min); - value = Math.min(value, max); - return value; -} - -export type RequiredExcept = T & Required>; - type RequiredOptional = Exclude< { [K in keyof T]: T extends Record ? never : K; diff --git a/src/utils/events.ts b/src/utils/events.ts index acab555..3845889 100644 --- a/src/utils/events.ts +++ b/src/utils/events.ts @@ -1,8 +1,15 @@ -import { MapPointerEvent } from '@2gis/mapgl/types'; -import { GltfPluginModelEvent, Id, ModelOptions, ModelTarget } from '../types'; +import type { DynamicObjectPointerEvent, GltfModel, Label } from '@2gis/mapgl/types'; +import { + BuildingState, + GltfPluginLabelEvent, + GltfPluginModelEvent, + LabelOptions, + LabelTarget, + ModelOptions, +} from '../types'; export const createModelEventData = ( - ev: MapPointerEvent, + ev: DynamicObjectPointerEvent, data: ModelOptions, ): GltfPluginModelEvent => ({ originalEvent: ev.originalEvent, @@ -14,3 +21,29 @@ export const createModelEventData = ( data, }, }); + +export const createLabelEvenData = ( + ev: DynamicObjectPointerEvent