From 0a007f62c6e43673b7538fd25c88d4734ed52994 Mon Sep 17 00:00:00 2001 From: Artemyev Alexander <32574343+alxart@users.noreply.github.com> Date: Fri, 1 Mar 2024 09:19:10 +0700 Subject: [PATCH] Realty Scene refactoring (#32) --- demo/index.html | 23 +- demo/index.ts | 12 +- demo/mocks.ts | 24 +- src/control/index.ts | 40 +- src/control/types.ts | 11 +- src/plugin.ts | 55 +- src/realtyScene/realtyScene.ts | 1175 ++++++++++++++------------------ src/types/events.ts | 9 +- src/types/plugin.ts | 6 +- src/utils/common.ts | 11 - src/utils/events.ts | 16 + 11 files changed, 614 insertions(+), 768 deletions(-) create mode 100644 src/utils/events.ts diff --git a/demo/index.html b/demo/index.html index 57e05db..abf7e69 100644 --- a/demo/index.html +++ b/demo/index.html @@ -6,25 +6,12 @@ diff --git a/demo/index.ts b/demo/index.ts index c68c381..76fc17a 100644 --- a/demo/index.ts +++ b/demo/index.ts @@ -10,7 +10,7 @@ async function start() { const map = new mapglAPI.Map('container', { center: [47.245286302641034, 56.134743473834099], - zoom: 17.9, + zoom: 18.9, key: 'cb20c5bf-34d3-4f0e-9b2b-33e9b8edb57f', pitch: 45, rotation: 330, @@ -20,7 +20,7 @@ async function start() { (window as any).map = map; const plugin = new GltfPlugin(map, { - modelsLoadStrategy: 'dontWaitAll', + modelsLoadStrategy: 'waitAll', modelsBaseUrl: 'https://disk.2gis.com/digital-twin/models_s3/realty_ads/zgktechnology/', floorsControl: { position: 'centerRight' }, poiConfig: { @@ -34,7 +34,7 @@ async function start() { hoverHighlight: { intencity: 0.1, }, - groundCoveringColor: 'rgba(233, 232, 220, 0.8)', + groundCoveringColor: 'rgba(0, 0, 0, 0.8)', }); (window as any).gltfPlugin = plugin; @@ -46,7 +46,7 @@ async function start() { .getContainer() .addEventListener('click', () => { plugin.removeRealtyScene(); - plugin.addRealtyScene(REALTY_SCENE, { modelId: '03a234cb', floorId: '235034' }); + plugin.addRealtyScene(REALTY_SCENE); }); new mapglAPI.Control(map, '', { @@ -63,7 +63,9 @@ async function start() { .getContainer() .addEventListener('click', () => { plugin.removeRealtyScene(); - plugin.addRealtyScene(REALTY_SCENE_1, { modelId: 'ds321ba234cb' }); + plugin.addRealtyScene(REALTY_SCENE_1, { + buildingId: 'ds321ba234cb', + }); }); new mapglAPI.Control(map, '', { diff --git a/demo/mocks.ts b/demo/mocks.ts index 8413ef0..7281603 100644 --- a/demo/mocks.ts +++ b/demo/mocks.ts @@ -7,6 +7,7 @@ export const REALTY_SCENE: BuildingOptions[] = [ modelUrl: 'zgktechnology1.glb', rotateZ: -15.1240072739039, linkedIds: ['70030076555823021'], + interactive: true, mapOptions: { center: [47.24547737708662, 56.134591508663135], pitch: 40, @@ -14,7 +15,7 @@ export const REALTY_SCENE: BuildingOptions[] = [ rotation: -41.4, }, popupOptions: { - coordinates: [47.24511721603574, 56.13451456056651], + coordinates: [47.24498128610925, 56.13451011334241], title: 'Корпус 1. 11 этажей', description: 'Срок сдачи: IV кв. 2024 г.
15 мин. пешком до ст. м. Московская', }, @@ -29,9 +30,10 @@ export const REALTY_SCENE: BuildingOptions[] = [ zoom: 20, rotation: -57.5, }, + isUnderground: true, poiGroups: [ { - id: 1111, + id: '1111', type: 'primary', minZoom: 19.5, elevation: 5, @@ -81,7 +83,6 @@ export const REALTY_SCENE: BuildingOptions[] = [ id: '000034', text: '11', modelUrl: 'zgktechnology1_floor11.glb', - isUnderground: true, mapOptions: { center: [47.24556663327373, 56.13456998211929], pitch: 40, @@ -90,7 +91,7 @@ export const REALTY_SCENE: BuildingOptions[] = [ }, poiGroups: [ { - id: 1111, + id: '1111', type: 'primary', minZoom: 19, elevation: 35, @@ -142,8 +143,9 @@ export const REALTY_SCENE: BuildingOptions[] = [ modelId: '1ba234cb', coordinates: [47.245286302641034, 56.134743473834099], modelUrl: 'zgktechnology2.glb', - rotateY: -15.1240072739039, + rotateZ: -15.1240072739039, linkedIds: ['70030076555821177'], + interactive: true, mapOptions: { center: [47.245008950283065, 56.1344698491912], pitch: 45, @@ -160,7 +162,6 @@ export const REALTY_SCENE: BuildingOptions[] = [ id: 'aaa777', text: '2-15', modelUrl: 'zgktechnology2_floor2.glb', - isUnderground: true, mapOptions: { center: [47.24463456947374, 56.134675042798094], pitch: 35, @@ -169,7 +170,7 @@ export const REALTY_SCENE: BuildingOptions[] = [ }, poiGroups: [ { - id: 1111, + id: '1111', type: 'primary', minZoom: 19.7, elevation: 7, @@ -255,7 +256,7 @@ export const REALTY_SCENE: BuildingOptions[] = [ }, poiGroups: [ { - id: 1111, + id: '1111', type: 'primary', minZoom: 18.9, elevation: 53, @@ -335,7 +336,7 @@ export const REALTY_SCENE: BuildingOptions[] = [ modelId: 'eda234cb', coordinates: [47.245286302641034, 56.134743473834099], modelUrl: 'zgktechnology_construction.glb', - rotateY: -15.1240072739039, + rotateZ: -15.1240072739039, linkedIds: ['70030076561388553'], interactive: false, }, @@ -346,8 +347,9 @@ export const REALTY_SCENE_1: BuildingOptions[] = [ modelId: 'ds321ba234cb', coordinates: [47.245286302641034, 56.134743473834099], modelUrl: 'zgktechnology2.glb', - rotateY: -15.1240072739039, + rotateZ: -15.1240072739039, linkedIds: ['70030076555823021', '70030076555821177', '70030076555823021'], + interactive: true, mapOptions: { center: [47.245008950283065, 56.1344698491912], pitch: 45, @@ -388,7 +390,7 @@ export const REALTY_SCENE_1: BuildingOptions[] = [ modelId: '345feda234cb', coordinates: [47.245286302641034, 56.134743473834099], modelUrl: 'zgktechnology_construction.glb', - rotateY: -15.1240072739039, + rotateZ: -15.1240072739039, linkedIds: ['70030076561388553'], interactive: false, }, diff --git a/src/control/index.ts b/src/control/index.ts index c4134e0..6c2e730 100644 --- a/src/control/index.ts +++ b/src/control/index.ts @@ -1,13 +1,11 @@ import type { Map as MapGL, ControlOptions } from '@2gis/mapgl/types'; - -import type { Id } from '../types/plugin'; -import type { ControlShowOptions } from './types'; +import type { ControlShowOptions, FloorLevel } from './types'; import icon_building from 'raw-loader!./icon_building.svg'; import icon_parking from 'raw-loader!./icon_parking.svg'; -import { Control } from './control'; import classes from './control.module.css'; -import { createCompoundId } from '../utils/common'; +import { Control } from './control'; +import { Id } from '../types'; const content = /* HTML */ `
@@ -31,7 +29,6 @@ const content = /* HTML */ ` * @internal */ export class GltfFloorControl extends Control { - private _map: MapGL; private _root: HTMLElement; private _content: HTMLElement; private _contentHome: HTMLElement; @@ -41,7 +38,6 @@ export class GltfFloorControl extends Control { constructor(map: MapGL, options: ControlOptions) { super(map, content, options); - this._map = map; this._root = this._wrap.querySelector(`.${classes.root}`) as HTMLElement; this._content = this._wrap.querySelector(`.${classes.content}`) as HTMLElement; this._contentHome = this._wrap.querySelector(`.${classes.contentHome}`) as HTMLElement; @@ -52,16 +48,16 @@ export class GltfFloorControl extends Control { public show(options: ControlShowOptions) { this._removeButtonsEventListeners(); - const { modelId, floorId, floorLevels = [] } = options; + const { buildingModelId, activeModelId, floorLevels = [] } = options; - this._currentFloorId = createCompoundId(modelId, floorId); + this._currentFloorId = activeModelId; this._root.style.display = 'block'; this._content.innerHTML = ''; this._contentHome.innerHTML = ''; let currentButton: HTMLElement | undefined; - floorLevels.forEach(({ floorId, text, icon }) => { - const rootContent = floorId === undefined ? this._contentHome : this._content; + floorLevels.forEach(({ modelId, text, icon }) => { + const rootContent = modelId === buildingModelId ? this._contentHome : this._content; const button = document.createElement('button'); let buttonContent = text; if (icon) { @@ -75,14 +71,13 @@ export class GltfFloorControl extends Control { } button.className = classes.control; button.innerHTML = `
${buttonContent}
`; - const id = createCompoundId(modelId, floorId); - button.name = id; - if (this._currentFloorId === id) { + button.name = modelId; + if (this._currentFloorId === modelId) { button.disabled = true; currentButton = button; } - const handler = this._controlHandler(modelId, floorId); + const handler = this._controlHandler(modelId); button.addEventListener('click', handler); this._handlers.set(button, handler); @@ -125,22 +120,19 @@ export class GltfFloorControl extends Control { }); } - private _controlHandler = (modelId: Id, floorId?: Id) => () => { - this.switchCurrentFloorLevel(modelId, floorId); + private _controlHandler = (modelId: Id) => () => { + this._switchCurrentFloorLevel(modelId); - this.emit('floorChange', { + this.emit('floorchange', { modelId, - floorId, }); }; - public switchCurrentFloorLevel(modelId: Id, floorId?: Id) { + private _switchCurrentFloorLevel(modelId: Id) { if (this._currentFloorId === undefined) { return; } - const id = createCompoundId(modelId, floorId); - const buttonToDisabled: HTMLButtonElement | null = this._wrap.querySelector( `.${classes.control}[name="${this._currentFloorId}"]`, ); @@ -149,12 +141,12 @@ export class GltfFloorControl extends Control { } const buttonToEnabled: HTMLButtonElement | null = this._wrap.querySelector( - `.${classes.control}[name="${id}"]`, + `.${classes.control}[name="${modelId}"]`, ); if (buttonToEnabled) { buttonToEnabled.disabled = true; } - this._currentFloorId = id; + this._currentFloorId = modelId; } } diff --git a/src/control/types.ts b/src/control/types.ts index 11bcdeb..453ce22 100644 --- a/src/control/types.ts +++ b/src/control/types.ts @@ -4,7 +4,7 @@ import { Id } from '../types/plugin'; * Floor level data */ export interface FloorLevel { - floorId?: Id; + modelId: Id; // id модели этажа или здания text: string; icon?: 'parking' | 'building' | string; } @@ -13,8 +13,8 @@ export interface FloorLevel { * Options for the method show */ export interface ControlShowOptions { - modelId: Id; - floorId?: Id; + buildingModelId: Id; + activeModelId: Id; floorLevels?: FloorLevel[]; } @@ -22,13 +22,12 @@ export interface ControlShowOptions { * Event that emitted on button presses of the control */ export interface FloorChangeEvent { - modelId: Id; - floorId?: Id; + modelId: Id; // id модели этажа или здания } export interface ControlEventTable { /** * Emitted when floor's plan was changed */ - floorChange: FloorChangeEvent; + floorchange: FloorChangeEvent; } diff --git a/src/plugin.ts b/src/plugin.ts index be0a459..bcf96f8 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -5,9 +5,11 @@ import type { Id, PluginOptions, ModelOptions, BuildingState } from './types/plu import { applyOptionalDefaults } from './utils/common'; import { Evented } from './external/evented'; -// import { RealtyScene } from './realtyScene/realtyScene'; 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'; interface Model { instance: any; // GltfModel @@ -26,7 +28,7 @@ export class GltfPlugin extends Evented { private map: MapGL; private options: Required; private models: Map; - // private realtyScene?: RealtyScene; + private realtyScene?: RealtyScene; /** * The main class of the plugin @@ -43,8 +45,8 @@ export class GltfPlugin extends Evented { * modelId: '03a234cb', * coordinates: [82.886554, 54.980988], * modelUrl: 'models/cube_draco.glb', - * rotateX: 90, - * scale: 1000, + * rotateZ: 90, + * scale: 2, * }, * ]); * ``` @@ -57,8 +59,15 @@ export class GltfPlugin extends Evented { this.map = map; this.options = applyOptionalDefaults(pluginOptions ?? {}, defaultOptions); this.models = new Map(); + + map.on('styleload', () => { + this.map.addLayer(GROUND_COVERING_LAYER); // мб унести отсюда в RealtyScene, нужно подумать + // this.poiGroups.onMapStyleUpdate(); + }); } + // public destroy() {} + public setOptions(pluginOptions: Pick, 'groundCoveringColor'>) { Object.keys(pluginOptions).forEach((option) => { switch (option) { @@ -71,8 +80,8 @@ export class GltfPlugin extends Evented { }); } - public async addModel(modelToLoad: ModelOptions, showOnLoad = true) { - return this.addModels([modelToLoad], showOnLoad ? [modelToLoad.modelId] : []); + public async addModel(modelToLoad: ModelOptions, hideOnLoad = false) { + return this.addModels([modelToLoad], hideOnLoad ? [] : [modelToLoad.modelId]); } public async addModels(modelsToLoad: ModelOptions[], modelIdsToShow?: Id[]) { @@ -123,6 +132,13 @@ export class GltfPlugin extends Evented { 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)); + }); + }, + ); }); }); @@ -139,6 +155,10 @@ export class GltfPlugin extends Evented { }); } + public isModelAdded(id: Id) { + return this.models.has(id); + } + public removeModel(id: Id) { const model = this.models.get(id); if (model) { @@ -168,21 +188,16 @@ export class GltfPlugin extends Evented { } public async addRealtyScene(scene: BuildingOptions[], state?: BuildingState) { - // this.realtyScene = new RealtyScene( - // this, - // this.map, - // this.eventSource, - // this.models, - // this.options, - // ); - // return this.realtyScene.add(scene, state); + this.realtyScene = new RealtyScene(this, this.map, this.options); + return this.realtyScene.init(scene, state); } - public removeRealtyScene(preserveCache?: boolean) { - // if (!this.realtyScene) { - // return; - // } - // this.realtyScene.destroy(preserveCache); - // this.realtyScene = undefined; + // public showRealtyScene() {} + + // public hideRealtyScene() {} + + public removeRealtyScene() { + this.realtyScene?.destroy(); + this.realtyScene = undefined; } } diff --git a/src/realtyScene/realtyScene.ts b/src/realtyScene/realtyScene.ts index 633d7da..f21f62f 100644 --- a/src/realtyScene/realtyScene.ts +++ b/src/realtyScene/realtyScene.ts @@ -1,663 +1,512 @@ -// import type { Map as MapGL, AnimationOptions, HtmlMarker, GeoJsonSource } from '@2gis/mapgl/types'; - -// import { GltfPlugin } from '../plugin'; -// import { defaultOptions } from '../defaultOptions'; -// import { GltfFloorControl } from '../control'; -// import { clone, createCompoundId } from '../utils/common'; -// import classes from './realtyScene.module.css'; - -// import type { Id, BuildingState, ModelOptions } from '../types/plugin'; -// import type { -// BuildingOptions, -// MapOptions, -// BuildingFloorOptions, -// PopupOptions, -// } from '../types/realtyScene'; -// import type { ControlShowOptions, FloorLevel, FloorChangeEvent } from '../control/types'; -// import type { -// GltfPluginModelEvent, -// GltfPluginPoiEvent, -// PoiGeoJsonProperties, -// } from '../types/events'; -// import { GROUND_COVERING_LAYER, GROUND_COVERING_SOURCE_DATA, GROUND_COVERING_SOURCE_PURPOSE } from '../constants'; - -// export class RealtyScene { -// private activeBuilding?: BuildingOptions; -// private activeModelId?: Id; -// private control?: GltfFloorControl; -// private activePoiGroupIds: Id[] = []; -// private container: HTMLElement; -// private buildingFacadeIds: Id[] = []; -// // this field is needed when the highlighted -// // model is placed under the floors' control -// private prevHoveredModelId: Id | null = null; -// private popup: HtmlMarker | null = null; -// private scene: BuildingOptions[] | null = null; -// private groundCoveringSource: GeoJsonSource; -// private undergroundFloors = new Set(); -// private poiGroups: PoiGroups; - -// constructor( -// private plugin: GltfPlugin, -// private map: MapGL, -// private eventSource: EventSource, -// private models: Map, -// private options: typeof defaultOptions, -// ) { -// this.poiGroups = new PoiGroups(this.map, this.options.poiConfig); -// this.container = map.getContainer(); -// this.groundCoveringSource = new mapgl.GeoJsonSource(map, { -// maxZoom: 2, -// data: GROUND_COVERING_SOURCE_DATA, -// attributes: { -// purpose: GROUND_COVERING_SOURCE_PURPOSE, -// }, -// }); - -// map.on('styleload', () => { -// this.map.addLayer(GROUND_COVERING_LAYER); -// this.poiGroups.onMapStyleUpdate();}) -// } - -// public async add(scene: BuildingOptions[], originalState?: BuildingState) { -// // make unique compound identifiers for floor's plans -// let state = originalState === undefined ? originalState : clone(originalState); -// this.makeUniqueFloorIds(scene); -// if (state?.floorId !== undefined) { -// state.floorId = createCompoundId(state.modelId, state.floorId); -// } - -// // set initial fields -// if (state !== undefined) { -// this.activeBuilding = scene.find((model) => model.modelId === state?.modelId); -// if (this.activeBuilding === undefined) { -// throw new Error( -// `There is no building's model with id ${state.modelId}. ` + -// `Please check options of method addRealtyScene`, -// ); -// } -// this.activeModelId = -// state.floorId !== undefined ? state.floorId : this.activeBuilding.modelId; -// } - -// // initialize initial scene -// const models: ModelOptions[] = []; -// const modelIds: Id[] = []; -// this.scene = scene; -// scene.forEach((scenePart) => { -// this.buildingFacadeIds.push(scenePart.modelId); -// const modelOptions = getBuildingModelOptions(scenePart); -// const floors = scenePart.floors ?? []; -// let hasFloorByDefault = false; - -// for (let floor of floors) { -// if (floor.isUnderground) { -// this.undergroundFloors.add(floor.id); -// } - -// if (state?.floorId !== undefined && floor.id === state.floorId) { -// // for convenience push original building -// models.push(modelOptions); -// // push modified options for floor -// models.push(getFloorModelOptions(floor, scenePart)); -// modelIds.push(floor.id); -// hasFloorByDefault = true; -// } -// } - -// if (!hasFloorByDefault) { -// models.push(modelOptions); -// modelIds.push(scenePart.modelId); -// } - -// if (this.options.modelsLoadStrategy === 'waitAll') { -// for (let floor of floors) { -// if (floor.id === state?.floorId) { -// continue; -// } -// models.push(getFloorModelOptions(floor, scenePart)); -// } -// } -// }); - -// // Leave only the underground floor's plan to be shown -// if (state?.floorId !== undefined && this.undergroundFloors.has(state.floorId)) { -// modelIds.length = 0; -// modelIds.push(state.floorId); -// } - -// return this.plugin.addModelsPartially(models, modelIds).then(() => { -// // set options after adding models -// if (state?.floorId !== undefined) { -// const floors = this.activeBuilding?.floors ?? []; -// const activeFloor = floors.find((floor) => floor.id === state?.floorId); -// this.setMapOptions(activeFloor?.mapOptions); -// this.addFloorPoi(activeFloor); -// if (this.undergroundFloors.has(state.floorId)) { -// this.switchOnGroundCovering(); -// } -// } else { -// this.setMapOptions(this.activeBuilding?.mapOptions); -// } - -// // initialize floors' control -// const { position } = this.options.floorsControl; -// this.control = new GltfFloorControl(this.map, { position }); -// if (state !== undefined) { -// const controlOptions = this.createControlOptions(scene, state); -// this.control?.show(controlOptions); -// if (state.floorId) { -// this.eventSource.setCurrentFloorId(state.floorId); -// } -// } - -// // bind all events -// this.bindRealtySceneEvents(); -// }); -// } - -// public resetGroundCoveringColor() { -// const attrs = this.groundCoveringSource.getAttributes(); -// if ('color' in attrs) { -// this.groundCoveringSource.setAttributes({ -// ...attrs, -// color: this.options.groundCoveringColor, -// }); -// } -// } - -// public isUndergroundFloorShown() { -// return this.activeModelId !== undefined && this.undergroundFloors.has(this.activeModelId); -// } - -// public destroy(preserveCache?: boolean) { -// this.unbindRealtySceneEvents(); - -// this.plugin.removeModels( -// this.scene?.reduce((agg, opts) => { -// agg.push(opts.modelId); -// opts.floors?.forEach((floor) => agg.push(floor.id)); - -// return agg; -// }, []) ?? [], -// preserveCache, -// ); - -// this.clearPoiGroups(); -// this.eventSource.setCurrentFloorId(null); - -// this.groundCoveringSource.destroy(); -// this.undergroundFloors.clear(); - -// this.control?.destroy(); -// this.control = undefined; - -// this.popup?.destroy(); -// this.popup = null; - -// this.activeBuilding = undefined; -// this.activeModelId = undefined; -// this.activePoiGroupIds = []; -// this.buildingFacadeIds = []; -// this.prevHoveredModelId = null; -// this.scene = null; -// } - -// private bindRealtySceneEvents() { -// this.plugin.on('click', this.onSceneClick); -// this.plugin.on('mouseover', this.onSceneMouseOver); -// this.plugin.on('mouseout', this.onSceneMouseOut); - -// this.control?.on('floorChange', this.floorChangeHandler); -// } - -// private unbindRealtySceneEvents() { -// this.plugin.off('click', this.onSceneClick); -// this.plugin.off('mouseover', this.onSceneMouseOver); -// this.plugin.off('mouseout', this.onSceneMouseOut); - -// this.control?.off('floorChange', this.floorChangeHandler); -// } - -// private createControlOptions(scene: BuildingOptions[], buildingState: BuildingState) { -// const { modelId, floorId } = buildingState; -// const options: ControlShowOptions = { -// modelId: modelId, -// }; -// if (floorId !== undefined) { -// options.floorId = floorId; -// } - -// const buildingData = scene.find((scenePart) => scenePart.modelId === modelId); -// if (!buildingData) { -// return options; -// } - -// if (buildingData.floors !== undefined) { -// const floorLevels: FloorLevel[] = [ -// { -// icon: 'building', -// text: '', -// }, -// ]; -// buildingData.floors.forEach((floor) => { -// floorLevels.push({ -// floorId: floor.id, -// text: floor.text, -// }); -// }); -// options.floorLevels = floorLevels; -// } -// return options; -// } - -// private setMapOptions(options?: MapOptions) { -// if (!options) { -// return; -// } - -// const animationOptions: AnimationOptions = { -// easing: 'easeInSine', -// duration: 500, -// }; -// if (options.center) { -// this.map.setCenter(options.center, animationOptions); -// } -// if (options.pitch) { -// this.map.setPitch(options.pitch, animationOptions); -// } -// if (options.rotation) { -// this.map.setRotation(options.rotation, animationOptions); -// } -// if (options.zoom) { -// this.map.setZoom(options.zoom, animationOptions); -// } -// } - -// // checks if the modelId is external facade of the building -// private isFacadeBuilding(modelId?: Id): modelId is Id { -// if (modelId === undefined) { -// return false; -// } - -// return this.buildingFacadeIds.includes(modelId); -// } - -// private getPopupOptions(modelId: Id): PopupOptions | undefined { -// if (this.scene === null) { -// return; -// } -// let building = this.scene.find((building) => building.modelId === modelId); -// if (building === undefined) { -// return; -// } -// return building.popupOptions; -// } - -// private onSceneMouseOver = (ev: GltfPluginPoiEvent | GltfPluginModelEvent) => { -// if (ev.target.type === 'model') { -// const id = ev.target.modelId; -// if (this.isFacadeBuilding(id)) { -// this.container.style.cursor = 'pointer'; -// this.toggleHighlightModel(id); -// let popupOptions = this.getPopupOptions(id); -// if (popupOptions) { -// this.showPopup(popupOptions); -// } -// } -// } -// }; -// private onSceneMouseOut = (ev: GltfPluginPoiEvent | GltfPluginModelEvent) => { -// if (ev.target.type === 'model') { -// const id = ev.target.modelId; -// if (this.isFacadeBuilding(id)) { -// this.container.style.cursor = ''; -// this.hidePopup(); -// if (this.prevHoveredModelId !== null) { -// this.toggleHighlightModel(id); -// } -// } -// } -// }; - -// private onSceneClick = (ev: GltfPluginPoiEvent | GltfPluginModelEvent) => { -// if (this.scene === null) { -// return; -// } - -// if (ev.target.type === 'model') { -// const id = ev.target.modelId; -// if (this.isFacadeBuilding(id)) { -// this.buildingClickHandler(this.scene, id); -// } -// } - -// if (ev.target.type === 'poi') { -// this.poiClickHandler(ev.target.data); -// } -// }; - -// private poiClickHandler = (data: PoiGeoJsonProperties) => { -// const url: string | undefined = data.userData.url; -// if (url !== undefined) { -// const a = document.createElement('a'); -// a.setAttribute('href', url); -// a.setAttribute('target', '_blank'); -// a.click(); -// } -// }; - -// private floorChangeHandler = (ev: FloorChangeEvent) => { -// const model = this.activeBuilding; -// if (model !== undefined && model.floors !== undefined) { -// if (this.popup !== null) { -// this.popup.destroy(); -// } - -// // click to the building button -// if (ev.floorId === undefined) { -// if (this.prevHoveredModelId !== null) { -// this.toggleHighlightModel(this.prevHoveredModelId); -// } - -// this.clearPoiGroups(); -// const modelsToAdd: ModelOptions[] = this.isUndergroundFloorShown() -// ? (this.scene ?? []).map((scenePart) => getBuildingModelOptions(scenePart)) -// : [getBuildingModelOptions(model)]; - -// this.plugin.addModels(modelsToAdd).then(() => { -// if (this.activeModelId !== undefined) { -// this.plugin.removeModel(this.activeModelId, true); -// if (this.isUndergroundFloorShown()) { -// this.switchOffGroundCovering(); -// } -// } -// this.setMapOptions(model?.mapOptions); -// this.activeModelId = model.modelId; -// }); -// } -// // click to the floor button -// if (ev.floorId !== undefined) { -// const selectedFloor = model.floors.find((floor) => floor.id === ev.floorId); -// if (selectedFloor !== undefined && this.activeModelId !== undefined) { -// const selectedFloorModelOption = getFloorModelOptions(selectedFloor, model); - -// // In case of underground -> underground and ground -> ground transitions just switch floor's plan -// if (this.isUndergroundFloorShown() === Boolean(selectedFloor.isUnderground)) { -// this.plugin.addModel(selectedFloorModelOption).then(() => { -// if (this.activeModelId !== undefined) { -// this.plugin.removeModel(this.activeModelId, true); -// } -// this.addFloorPoi(selectedFloor); -// }); - -// return; -// } - -// const modelsToAdd: ModelOptions[] = this.isUndergroundFloorShown() -// ? (this.scene ?? []) -// .filter((scenePart) => scenePart.modelId !== model.modelId) -// .map((scenePart) => getBuildingModelOptions(scenePart)) -// : []; - -// modelsToAdd.push(selectedFloorModelOption); - -// const modelsToRemove = this.isUndergroundFloorShown() -// ? [] -// : (this.scene ?? []) -// .filter((scenePart) => scenePart.modelId !== model.modelId) -// .map((scenePart) => scenePart.modelId); - -// modelsToRemove.push(this.activeModelId); - -// this.plugin.addModels(modelsToAdd).then(() => { -// this.plugin.removeModels(modelsToRemove, true); -// this.isUndergroundFloorShown() -// ? this.switchOffGroundCovering() -// : this.switchOnGroundCovering(); -// this.addFloorPoi(selectedFloor); -// }); -// } -// } -// } -// }; - -// private buildingClickHandler = (scene: BuildingOptions[], modelId: Id) => { -// const selectedBuilding = scene.find((model) => model.modelId === modelId); -// if (selectedBuilding === undefined) { -// return; -// } - -// // don't show the pointer cursor on the model when user -// // started to interact with the building -// this.container.style.cursor = ''; - -// if (this.popup !== null) { -// this.popup.destroy(); -// } - -// // if there is a visible floor plan, then show the external -// // facade of the active building before focusing on the new building -// if ( -// this.activeBuilding && -// this.activeModelId && -// this.activeModelId !== this.activeBuilding?.modelId -// ) { -// // User is able to click on any other buildings as long as ground floor's plan is shown -// // because when underground floor's plan is shown other buildings are hidden. -// const oldId = this.activeModelId; -// this.plugin.addModel(getBuildingModelOptions(this.activeBuilding)).then(() => { -// this.clearPoiGroups(); -// this.plugin.removeModel(oldId, true); -// }); -// } - -// // show the highest floor after a click on the building -// const floors = selectedBuilding.floors ?? []; -// if (floors.length !== 0) { -// const floorOptions = floors[floors.length - 1]; - -// const modelsToRemove = floorOptions.isUnderground -// ? scene.map((scenePart) => scenePart.modelId) -// : [selectedBuilding.modelId]; - -// this.plugin.addModel(getFloorModelOptions(floorOptions, selectedBuilding)).then(() => { -// this.plugin.removeModels(modelsToRemove, true); -// if (floorOptions.isUnderground) { -// this.switchOnGroundCovering(); -// } -// this.addFloorPoi(floorOptions); -// this.control?.switchCurrentFloorLevel(selectedBuilding.modelId, floorOptions.id); -// }); -// } else { -// this.activeModelId = selectedBuilding.modelId; -// this.setMapOptions(selectedBuilding.mapOptions); -// } - -// if ( -// this.activeBuilding === undefined || -// selectedBuilding.modelId !== this.activeBuilding?.modelId -// ) { -// // initialize control -// const { position } = this.options.floorsControl; -// this.control?.destroy(); -// this.control = new GltfFloorControl(this.map, { position }); -// const state = { modelId: selectedBuilding.modelId }; -// const controlOptions = this.createControlOptions(scene, state); -// this.control?.show(controlOptions); -// this.control.on('floorChange', (ev) => { -// this.floorChangeHandler(ev); -// }); -// } - -// this.activeBuilding = selectedBuilding; -// }; - -// 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); -// } - -// // TODO: Don't mutate scene data. -// private makeUniqueFloorIds(scene: BuildingOptions[]) { -// for (let scenePart of scene) { -// const floors = scenePart.floors ?? []; -// for (let floor of floors) { -// if (!floor.id.toString().startsWith(scenePart.modelId.toString())) { -// floor.id = createCompoundId(scenePart.modelId, floor.id); -// } -// } -// } -// } - -// public toggleHighlightModel(modelId: Id) { -// // skip toggle if user is using default emissiveIntensity -// // that means that model won't be hovered -// const { intencity } = this.options.hoverHighlight; -// if (intencity === 0) { -// return; -// } - -// const model = this.models.get(String(modelId)); - -// if (model === undefined) { -// return; -// } - -// let shouldUnsetFlag = false; -// model.traverse((obj) => { -// if (obj instanceof THREE.Mesh) { -// if (modelId === this.prevHoveredModelId) { -// obj.material.emissiveIntensity = 0.0; -// shouldUnsetFlag = true; -// } else { -// obj.material.emissiveIntensity = intencity; -// } -// } -// }); - -// this.prevHoveredModelId = shouldUnsetFlag ? null : modelId; -// this.map.triggerRerender(); -// } - -// private showPopup(options: PopupOptions) { -// this.popup = new mapgl.HtmlMarker(this.map, { -// coordinates: options.coordinates, -// html: this.getPopupHtml(options), -// }); -// } - -// private hidePopup() { -// if (this.popup !== null) { -// this.popup.destroy(); -// this.popup = null; -// } -// } - -// private getPopupHtml(data: PopupOptions) { -// if (data.description === undefined) { -// return `
-//

${data.title}

-//
`; -// } - -// return `
-//

${data.title}

-//

${data.description}

-//
`; -// } - -// private switchOffGroundCovering() { -// const attrs = { ...this.groundCoveringSource.getAttributes() }; -// delete attrs['color']; -// this.groundCoveringSource.setAttributes(attrs); -// } - -// private switchOnGroundCovering() { -// this.groundCoveringSource.setAttributes({ -// ...this.groundCoveringSource.getAttributes(), -// color: this.options.groundCoveringColor, -// }); -// } -// } - -// function getBuildingModelOptions(building: BuildingOptions): 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( -// floor: BuildingFloorOptions, -// building: BuildingOptions, -// ): ModelOptions { -// return { -// modelId: floor.id, -// coordinates: building.coordinates, -// modelUrl: floor.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, -// }; -// } +import type { Map as MapGL, AnimationOptions, HtmlMarker, GeoJsonSource } from '@2gis/mapgl/types'; + +import { GltfPlugin } from '../plugin'; +import { GltfFloorControl } from '../control'; +import classes from './realtyScene.module.css'; + +import type { BuildingState, Id, ModelOptions, PluginOptions } from '../types/plugin'; +import type { + BuildingOptions, + MapOptions, + BuildingFloorOptions, + PopupOptions, +} 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; +}; + +export class RealtyScene { + private buildings = new Map(); + private floors = new Map(); + private undergroundFloors = new Set(); + private state: RealtySceneState = { + activeModelId: undefined, + buildingVisibility: new Map(), + }; + + private groundCoveringSource: GeoJsonSource; + private control: GltfFloorControl; + private popup?: HtmlMarker; + + // private poiGroups: PoiGroups; + + constructor( + private plugin: GltfPlugin, + private map: MapGL, + private options: Required, + ) { + 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, + attributes: { + purpose: GROUND_COVERING_SOURCE_PURPOSE, + }, + }); + } + + private getBuildingModelId(id: Id | undefined) { + if (id === undefined) { + return; + } + + if (this.buildings.has(id)) { + return id; + } else { + const floor = this.floors.get(id); + if (floor) { + return floor.buildingOptions.modelId; + } + } + } + + private setState(newState: RealtySceneState) { + const prevState = this.state; + + 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 (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 (this.undergroundFloors.has(newState.activeModelId)) { + this.switchOnGroundCovering(); + } + } + } + + const prevBuildingModelId = this.getBuildingModelId(prevState.activeModelId); + const newBuildingModelId = this.getBuildingModelId(newState.activeModelId); + + if (prevBuildingModelId !== newBuildingModelId) { + if (newBuildingModelId !== undefined && newState.activeModelId !== undefined) { + const buildingOptions = this.buildings.get(newBuildingModelId); + if (buildingOptions) { + this.control.show({ + buildingModelId: buildingOptions.modelId, + activeModelId: newState.activeModelId, + floorLevels: [ + { + modelId: buildingOptions.modelId, + icon: 'building', + text: '', + }, + ...buildingOptions.floors, + ], + }); + } + } + } + + this.state = newState; + } + + public async init(scene: BuildingOptions[], state?: BuildingState) { + // Приводим стейт пользователя к внутреннему виду id + let activeModelId: Id | undefined = state + ? state.floorId + ? getFloorModelId(state.buildingId, state.floorId) + : state.buildingId + : undefined; + + scene.forEach((building) => { + const { floors, ...buildingPart } = building; + const internalBuilding: BuildingOptionsInternal = { + ...buildingPart, + floors: [], + }; + const buildingOptions = getBuildingModelOptions(internalBuilding); + + (floors ?? []).forEach((floor) => { + const floorModelId = getFloorModelId(building.modelId, floor.id); + internalBuilding.floors.push({ + modelId: floorModelId, + text: floor.text, + icon: floor.icon, + }); + + this.floors.set(floorModelId, { + ...floor, + buildingOptions: buildingOptions, + }); + + if (floor.isUnderground) { + this.undergroundFloors.add(floorModelId); + } + }); + + this.buildings.set(building.modelId, internalBuilding); + }); + + // Оставляем только существующее значение из переданных modelId в scene + activeModelId = + activeModelId !== undefined && + (this.buildings.has(activeModelId) || this.floors.has(activeModelId)) + ? activeModelId + : undefined; + + const modelsToLoad: Map = new Map(); + const buildingVisibility: Map = new Map(); + + this.buildings.forEach((options, id) => { + const modelOptions = getBuildingModelOptions(options); + modelsToLoad.set(id, modelOptions); + buildingVisibility.set(id, modelOptions); + }); + + if (activeModelId) { + const floorOptions = this.floors.get(activeModelId); + if (floorOptions) { + if (this.undergroundFloors.has(activeModelId)) { + buildingVisibility.clear(); // показываем только подземный этаж + } + + const modelOptions = getFloorModelOptions(floorOptions); + buildingVisibility.set(floorOptions.buildingOptions.modelId, modelOptions); + modelsToLoad.set(activeModelId, modelOptions); + } + } + + if (this.options.modelsLoadStrategy === 'waitAll') { + this.floors.forEach((options, id) => + modelsToLoad.set(id, getFloorModelOptions(options)), + ); + } + + return this.plugin + .addModels(Array.from(modelsToLoad.values()), Array.from(buildingVisibility.keys())) + .then(() => { + this.setState({ + activeModelId, + buildingVisibility, + }); + + this.plugin.on('click', this.onSceneClick); + this.plugin.on('mouseover', this.onSceneMouseOver); + this.plugin.on('mouseout', this.onSceneMouseOut); + this.control.on('floorchange', this.floorChangeHandler); + }); + } + + public resetGroundCoveringColor() { + const attrs = this.groundCoveringSource.getAttributes(); + if ('color' in attrs) { + this.groundCoveringSource.setAttributes({ + ...attrs, + color: this.options.groundCoveringColor, + }); + } + } + + public destroy() { + 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.plugin.removeModels([...this.buildings.keys(), ...this.floors.keys()]); + + // this.clearPoiGroups(); + + this.groundCoveringSource.destroy(); + this.undergroundFloors.clear(); + + this.control.destroy(); + + this.popup?.destroy(); + this.popup = undefined; + + this.state.activeModelId = undefined; + this.state.buildingVisibility.clear(); + this.buildings.clear(); + this.floors.clear(); + } + + private setMapOptions(options?: MapOptions) { + if (!options) { + return; + } + + const animationOptions: AnimationOptions = { + easing: 'easeInSine', + duration: 500, + }; + + if (options.center) { + this.map.setCenter(options.center, animationOptions); + } + if (options.pitch) { + this.map.setPitch(options.pitch, animationOptions); + } + if (options.rotation) { + this.map.setRotation(options.rotation, animationOptions); + } + if (options.zoom) { + this.map.setZoom(options.zoom, animationOptions); + } + } + + private onSceneMouseOut = (ev: GltfPluginPoiEvent | GltfPluginModelEvent) => { + if (ev.target.type !== 'model') { + return; + } + + this.popup?.destroy(); + }; + + private onSceneMouseOver = ({ target }: GltfPluginPoiEvent | GltfPluginModelEvent) => { + if (target.type === 'poi' || target.modelId === undefined) { + return; + } + + const options = this.buildings.get(target.modelId); + if (!options || !options.popupOptions) { + return; + } + + this.popup = new mapgl.HtmlMarker(this.map, { + coordinates: options.popupOptions.coordinates, + html: getPopupHtml(options.popupOptions), + interactive: false, + }); + }; + + private onSceneClick = ({ target }: GltfPluginPoiEvent | GltfPluginModelEvent) => { + if (target.type === 'model') { + const options = this.buildings.get(target.modelId); + if (options) { + this.buildingClickHandler(target.modelId); + } + } else if (target.type === 'poi') { + const userData = target.data.userData; + if (isObject(userData) && typeof userData.url === 'string') { + const a = document.createElement('a'); + a.setAttribute('href', userData.url); + a.setAttribute('target', '_blank'); + a.click(); + } + } + }; + + private floorChangeHandler = (ev: FloorChangeEvent) => { + const buildingVisibility: Map = new Map(); + this.buildings.forEach((options, id) => { + buildingVisibility.set(id, getBuildingModelOptions(options)); + }); + const buildingOptions = this.buildings.get(ev.modelId); + if (buildingOptions) { + this.setState({ + activeModelId: ev.modelId, + buildingVisibility, + }); + return; + } + + const floorOptions = this.floors.get(ev.modelId); + if (floorOptions) { + if (this.undergroundFloors.has(ev.modelId)) { + buildingVisibility.clear(); + } + buildingVisibility.set( + floorOptions.buildingOptions.modelId, + getFloorModelOptions(floorOptions), + ); + this.setState({ + activeModelId: ev.modelId, + buildingVisibility, + }); + return; + } + }; + + private buildingClickHandler = (modelId: Id) => { + const buildingOptions = this.buildings.get(modelId); + if (!buildingOptions) { + return; + } + + let activeModelId = modelId; + const buildingVisibility: Map = new Map(); + this.buildings.forEach((options, id) => { + buildingVisibility.set(id, getBuildingModelOptions(options)); + }); + + // показываем самый высокий этаж здания после клика + const floors = buildingOptions.floors ?? []; + if (floors.length) { + const { modelId: floorModelId } = floors[floors.length - 1]; + const floorOptions = this.floors.get(floorModelId); + if (floorOptions) { + activeModelId = floorModelId; + if (this.undergroundFloors.has(floorModelId)) { + buildingVisibility.clear(); + } + buildingVisibility.set( + floorOptions.buildingOptions.modelId, + getFloorModelOptions(floorOptions), + ); + } + } + + this.setState({ + buildingVisibility, + activeModelId, + }); + }; + + // 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']; + this.groundCoveringSource.setAttributes(attrs); + } + + private switchOnGroundCovering() { + this.groundCoveringSource.setAttributes({ + ...this.groundCoveringSource.getAttributes(), + color: this.options.groundCoveringColor, + }); + } +} + +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 2d68965..6db4834 100644 --- a/src/types/events.ts +++ b/src/types/events.ts @@ -29,14 +29,9 @@ export interface ModelTarget { data: ModelOptions; /** - * Identifier of the building's model + * Identifier of the building's or floor's model */ - modelId?: Id; - - /** - * Identifier of the current floor - */ - floorId?: Id; + modelId: Id; } export interface PoiTarget { diff --git a/src/types/plugin.ts b/src/types/plugin.ts index f389391..9dacf36 100644 --- a/src/types/plugin.ts +++ b/src/types/plugin.ts @@ -1,4 +1,4 @@ -export type Id = string | number; +export type Id = string; export type ColorModelString = `${'rgb' | 'hsl'}(${string})`; export type HexColorString = `#${string}`; @@ -110,12 +110,12 @@ export interface BuildingState { /** * Identifier of the building's model */ - modelId: Id; + buildingId: string; /** * Identifier of the floor's model */ - floorId?: Id; + floorId?: string; } /** diff --git a/src/utils/common.ts b/src/utils/common.ts index f5b4c55..c9f3f74 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -4,17 +4,6 @@ export function clamp(value: number, min: number, max: number): number { return value; } -export function clone(obj: T): T { - return JSON.parse(JSON.stringify(obj)); -} - -export function createCompoundId(modelId: string | number, floorId?: string | number) { - if (floorId === undefined) { - return String(modelId); - } - return `${modelId}_${floorId}`; -} - export type RequiredExcept = T & Required>; type RequiredOptional = Exclude< diff --git a/src/utils/events.ts b/src/utils/events.ts new file mode 100644 index 0000000..acab555 --- /dev/null +++ b/src/utils/events.ts @@ -0,0 +1,16 @@ +import { MapPointerEvent } from '@2gis/mapgl/types'; +import { GltfPluginModelEvent, Id, ModelOptions, ModelTarget } from '../types'; + +export const createModelEventData = ( + ev: MapPointerEvent, + data: ModelOptions, +): GltfPluginModelEvent => ({ + originalEvent: ev.originalEvent, + point: ev.point, + lngLat: ev.lngLat, + target: { + type: 'model', + modelId: data.modelId, + data, + }, +});