From f7f62f5707933c8bba2eb56c7a6c3e36363b2845 Mon Sep 17 00:00:00 2001 From: "hung.pv" Date: Wed, 9 Apr 2025 13:53:41 +0700 Subject: [PATCH 01/35] feat: dataset --- libs/map/dataset/.eslintrc.json | 30 ++++++++++++++++++ libs/map/dataset/README.md | 7 +++++ libs/map/dataset/package.json | 8 +++++ libs/map/dataset/project.json | 32 ++++++++++++++++++++ libs/map/dataset/src/index.ts | 1 + libs/map/dataset/src/lib/dataset.ts | 3 ++ libs/map/dataset/tsconfig.json | 19 ++++++++++++ libs/map/dataset/tsconfig.lib.json | 10 ++++++ libs/map/dataset/vite.config.ts | 47 +++++++++++++++++++++++++++++ nx.json | 3 +- tsconfig.base.json | 1 + 11 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 libs/map/dataset/.eslintrc.json create mode 100644 libs/map/dataset/README.md create mode 100644 libs/map/dataset/package.json create mode 100644 libs/map/dataset/project.json create mode 100644 libs/map/dataset/src/index.ts create mode 100644 libs/map/dataset/src/lib/dataset.ts create mode 100644 libs/map/dataset/tsconfig.json create mode 100644 libs/map/dataset/tsconfig.lib.json create mode 100644 libs/map/dataset/vite.config.ts diff --git a/libs/map/dataset/.eslintrc.json b/libs/map/dataset/.eslintrc.json new file mode 100644 index 0000000..7b25f23 --- /dev/null +++ b/libs/map/dataset/.eslintrc.json @@ -0,0 +1,30 @@ +{ + "extends": ["../../../.eslintrc.base.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.json"], + "parser": "jsonc-eslint-parser", + "rules": { + "@nx/dependency-checks": [ + "error", + { + "ignoredFiles": ["{projectRoot}/vite.config.{js,ts,mjs,mts}"] + } + ] + } + } + ] +} diff --git a/libs/map/dataset/README.md b/libs/map/dataset/README.md new file mode 100644 index 0000000..c8764ac --- /dev/null +++ b/libs/map/dataset/README.md @@ -0,0 +1,7 @@ +# dataset + +This library was generated with [Nx](https://nx.dev). + +## Building + +Run `nx build dataset` to build the library. diff --git a/libs/map/dataset/package.json b/libs/map/dataset/package.json new file mode 100644 index 0000000..cde020c --- /dev/null +++ b/libs/map/dataset/package.json @@ -0,0 +1,8 @@ +{ + "name": "@hungpvq/vue-map-dataset", + "version": "0.0.1", + "dependencies": {}, + "main": "./index.js", + "module": "./index.mjs", + "typings": "./index.d.ts" +} diff --git a/libs/map/dataset/project.json b/libs/map/dataset/project.json new file mode 100644 index 0000000..89ee923 --- /dev/null +++ b/libs/map/dataset/project.json @@ -0,0 +1,32 @@ +{ + "name": "dataset", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/map/dataset/src", + "projectType": "library", + "release": { + "version": { + "generatorOptions": { + "packageRoot": "dist\\{projectRoot}", + "currentVersionResolver": "git-tag" + } + } + }, + "tags": [], + "targets": { + "build": { + "executor": "@nx/vite:build", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/libs/map/dataset", + "main": "libs/map/dataset/src/index.ts", + "tsConfig": "libs/map/dataset/tsconfig.lib.json", + "assets": ["libs/map/dataset/*.md"] + } + }, + "nx-release-publish": { + "options": { + "packageRoot": "dist\\{projectRoot}" + } + } + } +} diff --git a/libs/map/dataset/src/index.ts b/libs/map/dataset/src/index.ts new file mode 100644 index 0000000..36d9c74 --- /dev/null +++ b/libs/map/dataset/src/index.ts @@ -0,0 +1 @@ +export * from './lib/dataset'; diff --git a/libs/map/dataset/src/lib/dataset.ts b/libs/map/dataset/src/lib/dataset.ts new file mode 100644 index 0000000..525d0e6 --- /dev/null +++ b/libs/map/dataset/src/lib/dataset.ts @@ -0,0 +1,3 @@ +export function dataset(): string { + return 'dataset'; +} diff --git a/libs/map/dataset/tsconfig.json b/libs/map/dataset/tsconfig.json new file mode 100644 index 0000000..f2400ab --- /dev/null +++ b/libs/map/dataset/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/libs/map/dataset/tsconfig.lib.json b/libs/map/dataset/tsconfig.lib.json new file mode 100644 index 0000000..be3b140 --- /dev/null +++ b/libs/map/dataset/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "types": ["node", "vite/client"] + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/libs/map/dataset/vite.config.ts b/libs/map/dataset/vite.config.ts new file mode 100644 index 0000000..56e4029 --- /dev/null +++ b/libs/map/dataset/vite.config.ts @@ -0,0 +1,47 @@ +/// +import { defineConfig } from 'vite'; +import dts from 'vite-plugin-dts'; +import * as path from 'path'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + +export default defineConfig({ + root: __dirname, + cacheDir: '../../../node_modules/.vite/libs/map/dataset', + + plugins: [ + nxViteTsPaths(), + dts({ + entryRoot: 'src', + tsconfigPath: path.join(__dirname, 'tsconfig.lib.json'), + }), + ], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + + // Configuration for building your library. + // See: https://vitejs.dev/guide/build.html#library-mode + build: { + outDir: '../../../dist/libs/map/dataset', + emptyOutDir: true, + reportCompressedSize: true, + commonjsOptions: { + transformMixedEsModules: true, + }, + lib: { + // Could also be a dictionary or array of multiple entry points. + entry: 'src/index.ts', + name: 'dataset', + fileName: 'index', + // Change this to the formats you want to support. + // Don't forget to update your package.json as well. + formats: ['es', 'cjs'], + }, + rollupOptions: { + // External packages that should not be bundled into your library. + external: [], + }, + }, +}); diff --git a/nx.json b/nx.json index 02efecf..74472cc 100644 --- a/nx.json +++ b/nx.json @@ -80,6 +80,7 @@ "release": { "projectsRelationship": "independent", "version": { + "preVersionCommand": "npx nx run-many -t build", "conventionalCommits": true }, "changelog": { @@ -94,6 +95,6 @@ }, "automaticFromRef": true }, - "projects": ["libs/*"] + "projects": ["libs/*", "dataset"] } } diff --git a/tsconfig.base.json b/tsconfig.base.json index 3fc5f3d..6f6d5ab 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -26,6 +26,7 @@ "@hungpvq/vue-draggable": ["libs/draggable/src/index.ts"], "@hungpvq/vue-map-basemap": ["libs/map/basemap/src/index.ts"], "@hungpvq/vue-map-core": ["libs/map/core/src/index.ts"], + "@hungpvq/vue-map-dataset": ["libs/map/dataset/src/index.ts"], "@hungpvq/vue-map-draw": ["libs/map/draw/src/index.ts"], "@hungpvq/vue-map-layer": ["libs/map/layer/src/index.ts"], "@hungpvq/vue-map-measurement": ["libs/map/measurement/src/index.ts"], From 24d35df7ee46cf358575f393de8cf77aeb062d12 Mon Sep 17 00:00:00 2001 From: "hung.pv" Date: Wed, 9 Apr 2025 18:28:00 +0700 Subject: [PATCH 02/35] feat: init dataset --- apps/demo-map/src/router/index.ts | 4 + apps/demo-map/src/views/test.vue | 113 ++++ libs/map/core/src/types/layer.ts | 2 +- libs/map/dataset/src/index.ts | 8 +- .../dataset/src/interfaces/dataset.base.ts | 70 +++ .../dataset/src/interfaces/dataset.handler.ts | 21 + .../map/dataset/src/interfaces/dataset.map.ts | 8 + .../dataset/src/interfaces/dataset.visitor.ts | 28 + .../interfaces/part-list-view-ui.interface.ts | 25 + libs/map/dataset/src/lib/dataset.ts | 3 - libs/map/dataset/src/model/base.ts | 11 + libs/map/dataset/src/model/dataset.base.ts | 173 ++++++ libs/map/dataset/src/model/dataset.handler.ts | 51 ++ .../map/dataset/src/model/dataset.visitors.ts | 505 ++++++++++++++++++ .../src/model/part-list-view-ui.model.ts | 21 + .../map/dataset/src/parts/dataset.handlers.ts | 95 ++++ 16 files changed, 1133 insertions(+), 5 deletions(-) create mode 100644 apps/demo-map/src/views/test.vue create mode 100644 libs/map/dataset/src/interfaces/dataset.base.ts create mode 100644 libs/map/dataset/src/interfaces/dataset.handler.ts create mode 100644 libs/map/dataset/src/interfaces/dataset.map.ts create mode 100644 libs/map/dataset/src/interfaces/dataset.visitor.ts create mode 100644 libs/map/dataset/src/interfaces/part-list-view-ui.interface.ts delete mode 100644 libs/map/dataset/src/lib/dataset.ts create mode 100644 libs/map/dataset/src/model/base.ts create mode 100644 libs/map/dataset/src/model/dataset.base.ts create mode 100644 libs/map/dataset/src/model/dataset.handler.ts create mode 100644 libs/map/dataset/src/model/dataset.visitors.ts create mode 100644 libs/map/dataset/src/model/part-list-view-ui.model.ts create mode 100644 libs/map/dataset/src/parts/dataset.handlers.ts diff --git a/apps/demo-map/src/router/index.ts b/apps/demo-map/src/router/index.ts index be2bc94..8c95c34 100644 --- a/apps/demo-map/src/router/index.ts +++ b/apps/demo-map/src/router/index.ts @@ -29,6 +29,10 @@ const router = createRouter({ path: '/geojson/', component: () => import('../views/GeojsonIo/index.vue'), }, + { + path: '/test/', + component: () => import('../views/test.vue'), + }, ], }); diff --git a/apps/demo-map/src/views/test.vue b/apps/demo-map/src/views/test.vue new file mode 100644 index 0000000..e4225b4 --- /dev/null +++ b/apps/demo-map/src/views/test.vue @@ -0,0 +1,113 @@ + + + diff --git a/libs/map/core/src/types/layer.ts b/libs/map/core/src/types/layer.ts index 558893b..3b92c7d 100644 --- a/libs/map/core/src/types/layer.ts +++ b/libs/map/core/src/types/layer.ts @@ -44,7 +44,7 @@ export type IListView = IView & { index: number; group?: IGroupListView; multi: boolean; - show: any; + show?: boolean; }; // === list end === diff --git a/libs/map/dataset/src/index.ts b/libs/map/dataset/src/index.ts index 36d9c74..a410b18 100644 --- a/libs/map/dataset/src/index.ts +++ b/libs/map/dataset/src/index.ts @@ -1 +1,7 @@ -export * from './lib/dataset'; +export * from './interfaces/dataset.base'; +export * from './interfaces/dataset.handler'; +export * from './interfaces/dataset.visitor'; +export * from './model/dataset.base'; +export * from './model/dataset.handler'; +export * from './model/dataset.visitors'; +export * from './parts/dataset.handlers'; diff --git a/libs/map/dataset/src/interfaces/dataset.base.ts b/libs/map/dataset/src/interfaces/dataset.base.ts new file mode 100644 index 0000000..afeaa40 --- /dev/null +++ b/libs/map/dataset/src/interfaces/dataset.base.ts @@ -0,0 +1,70 @@ +import { IDatasetVisitor } from './dataset.visitor'; + +/** + * Base interface for the Dataset Composite pattern + * This interface defines the common operations for both leaf and composite nodes + */ +export interface IDataset extends IDatasetVisit { + get id(): string; + /** + * Get the name of the dataset + */ + getName(): string; + /** + * Set the name of the dataset + */ + setName(name: string): void; + + /** + * Get the data of the dataset + */ + getData(): any; + /** + * Get the data of the dataset + */ + setData(data: any): void; + + /** + * Add a child dataset (only applicable for composite nodes) + * @param dataset The dataset to add as a child + */ + add?(dataset: IDataset): void; + + /** + * Remove a child dataset (only applicable for composite nodes) + * @param dataset The dataset to remove + */ + remove?(dataset: IDataset): void; + + /** + * Get all child datasets (only applicable for composite nodes) + */ + getChildren?(): IDataset[]; +} + +export interface IDatasetVisit { + /** + * Get the parent dataset + * @returns The parent dataset or undefined if this is a root dataset + */ + getParent(): IDataset | undefined; + + /** + * Set the parent dataset + * @param parent The parent dataset to set + */ + setParent(parent?: IDataset): void; + + /** + * Check if this dataset is a composite (has children) + * @returns True if this dataset is a composite, false otherwise + */ + isComposite(): boolean; + + /** + * Accept a visitor + * @param visitor The visitor to accept + * @returns The result of the visitor's visit + */ + accept(visitor: IDatasetVisitor): any; +} diff --git a/libs/map/dataset/src/interfaces/dataset.handler.ts b/libs/map/dataset/src/interfaces/dataset.handler.ts new file mode 100644 index 0000000..45e24cd --- /dev/null +++ b/libs/map/dataset/src/interfaces/dataset.handler.ts @@ -0,0 +1,21 @@ +import { IDataset } from './dataset.base'; + +/** + * Interface for handlers in the Chain of Responsibility pattern + * Each handler can process a dataset or pass it to the next handler + */ +export interface IDatasetHandler { + /** + * Set the next handler in the chain + * @param handler The next handler + * @returns The next handler for method chaining + */ + setNext(handler: IDatasetHandler): IDatasetHandler; + + /** + * Process the dataset + * @param dataset The dataset to process + * @returns The processed dataset or null if the handler cannot process it + */ + handle(dataset: IDataset): IDataset | null; +} diff --git a/libs/map/dataset/src/interfaces/dataset.map.ts b/libs/map/dataset/src/interfaces/dataset.map.ts new file mode 100644 index 0000000..a164c56 --- /dev/null +++ b/libs/map/dataset/src/interfaces/dataset.map.ts @@ -0,0 +1,8 @@ +import type { MapSimple } from '@hungpvq/shared-map'; + +export interface IDatasetMap { + addToMap(map: MapSimple, beforeId?: string): void; + removeFromMap(map: MapSimple): void; + getExecutionOrder?(functionName: string): number; + setExecutionOrder?(functionName: string, order: number): void; +} diff --git a/libs/map/dataset/src/interfaces/dataset.visitor.ts b/libs/map/dataset/src/interfaces/dataset.visitor.ts new file mode 100644 index 0000000..cb28a5b --- /dev/null +++ b/libs/map/dataset/src/interfaces/dataset.visitor.ts @@ -0,0 +1,28 @@ +import { IDataset } from './dataset.base'; + +/** + * Interface for visitors in the Visitor pattern + * Each visitor can visit different types of dataset nodes + */ +export interface IDatasetVisitor { + /** + * Visit a leaf dataset + * @param dataset The leaf dataset to visit + * @returns The result of visiting the leaf dataset + */ + visitLeaf(dataset: IDataset): any; + + /** + * Visit a composite dataset + * @param dataset The composite dataset to visit + * @returns The result of visiting the composite dataset + */ + visitComposite(dataset: IDataset): any; + + /** + * Visit a root dataset (top-level dataset) + * @param dataset The root dataset to visit + * @returns The result of visiting the root dataset + */ + visitRoot(dataset: IDataset): any; +} diff --git a/libs/map/dataset/src/interfaces/part-list-view-ui.interface.ts b/libs/map/dataset/src/interfaces/part-list-view-ui.interface.ts new file mode 100644 index 0000000..3bb0e8c --- /dev/null +++ b/libs/map/dataset/src/interfaces/part-list-view-ui.interface.ts @@ -0,0 +1,25 @@ +import { Color } from '@hungpvq/shared-map'; + +// === list === +export type IGroupListViewUI = + | string + | { + name: string; + id: string; + children?: T[]; + }; +export type IListViewUI = { + name?: string; + opacity: number; + selected: boolean; + metadata: any; + color: Color; + config: { + disable_delete?: boolean; + disabled_opacity?: boolean; + component?: any; + }; + index: number; + group?: IGroupListViewUI; + show: any; +}; diff --git a/libs/map/dataset/src/lib/dataset.ts b/libs/map/dataset/src/lib/dataset.ts deleted file mode 100644 index 525d0e6..0000000 --- a/libs/map/dataset/src/lib/dataset.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function dataset(): string { - return 'dataset'; -} diff --git a/libs/map/dataset/src/model/base.ts b/libs/map/dataset/src/model/base.ts new file mode 100644 index 0000000..6ce1de5 --- /dev/null +++ b/libs/map/dataset/src/model/base.ts @@ -0,0 +1,11 @@ +import { getUUIDv4 } from '@hungpvq/shared'; + +export class Base { + private _id: string; + get id() { + return this._id; + } + constructor() { + this._id = `${getUUIDv4()}`; + } +} diff --git a/libs/map/dataset/src/model/dataset.base.ts b/libs/map/dataset/src/model/dataset.base.ts new file mode 100644 index 0000000..0e1f4b8 --- /dev/null +++ b/libs/map/dataset/src/model/dataset.base.ts @@ -0,0 +1,173 @@ +import { IDataset } from '../interfaces/dataset.base'; +import { IDatasetVisitor } from '../interfaces/dataset.visitor'; +import { Base } from './base'; + +/** + * The base DatasetComponent class declares common operations for both simple and + * complex objects of a composition. + */ +abstract class DatasetComponent extends Base implements IDataset { + protected parent?: DatasetComponent; + protected name: string; + protected data: any; + protected executionOrders: Map = new Map(); + + constructor(name: string, data?: any) { + super(); + this.name = name; + this.data = data; + } + public setParent(parent?: DatasetComponent) { + this.parent = parent; + } + + public getParent(): DatasetComponent | undefined { + return this.parent; + } + /** + * Accept a visitor + * This method is part of the Visitor pattern + * @param visitor The visitor to accept + * @returns The result of the visitor's visit + */ + accept(visitor: IDatasetVisitor): any { + // By default, leaf nodes are visited as leaves + return visitor.visitLeaf(this); + } + + /** + * You can provide a method that lets the client code figure out whether a + * Datasetcomponent can bear children. + */ + public isComposite(): boolean { + return false; + } + + getName(): string { + return this.name; + } + + setName(name: string) { + this.name = name; + } + + getData(): any { + return this.data; + } + setData(data: any) { + this.data = data; + } + + /** + * Get the execution order for a specific function + * Lower values are executed first + * @param functionName The name of the function + * @returns The execution order value + */ + getExecutionOrder(functionName: string): number { + return this.executionOrders.get(functionName) ?? 5; // Default order is 5 + } + + /** + * Set the execution order for a specific function + * @param functionName The name of the function + * @param order The execution order value (lower values are executed first) + */ + setExecutionOrder(functionName: string, order: number): void { + this.executionOrders.set(functionName, order); + } +} +/** + * Leaf node in the Dataset Composite pattern + * Represents a single dataset without children + */ +export class DatasetLeaf extends DatasetComponent { + constructor(name: string, data: any) { + super(name, data); + } + + /** + * Accept a visitor + * @param visitor The visitor to accept + * @returns The result of the visitor's visit + */ + override accept(visitor: IDatasetVisitor): any { + return visitor.visitLeaf(this); + } +} + +/** + * Composite node in the Dataset Composite pattern + * Represents a dataset that can contain other datasets + */ +export class DatasetComposite extends DatasetComponent { + private children: IDataset[] = []; + + constructor(name: string) { + super(name); + } + + override isComposite(): boolean { + return true; + } + + override getData(): any { + // For composite nodes, we might want to aggregate data from all children + // or provide a different representation + return this.children.map((child) => child.getData()); + } + + add(dataset: IDataset): void { + this.children.push(dataset); + if (dataset instanceof DatasetComponent) { + dataset.setParent(this); + } + } + + remove(dataset: IDataset): void { + const index = this.children.findIndex((child) => child === dataset); + if (index !== -1) { + this.children.splice(index, 1); + if (dataset instanceof DatasetComponent) { + dataset.setParent(undefined); + } + } + } + + getChildren(): IDataset[] { + return [...this.children]; + } + + /** + * Accept a visitor + * @param visitor The visitor to accept + * @returns The result of the visitor's visit + */ + override accept(visitor: IDatasetVisitor): any { + // Check if this is a root node (no parent) + if (!this.parent) { + return visitor.visitRoot(this); + } else { + return visitor.visitComposite(this); + } + } +} + +/** + * Factory function to create a dataset + * @param name The name of the dataset + * @param data Optional data for leaf nodes + * @param isComposite Whether to create a composite node + * @returns A new dataset instance + */ +export function createDataset( + name: string, + data?: any, + isComposite = false +): IDataset { + if (isComposite) { + return new DatasetComposite(name); + } else { + return new DatasetLeaf(name, data); + } +} diff --git a/libs/map/dataset/src/model/dataset.handler.ts b/libs/map/dataset/src/model/dataset.handler.ts new file mode 100644 index 0000000..ded73eb --- /dev/null +++ b/libs/map/dataset/src/model/dataset.handler.ts @@ -0,0 +1,51 @@ +import { IDataset } from '../interfaces/dataset.base'; +import { IDatasetHandler } from '../interfaces/dataset.handler'; + +/** + * Abstract base class for dataset handlers in the Chain of Responsibility pattern + * Implements common functionality for all handlers + */ +export abstract class DatasetHandlerBase implements IDatasetHandler { + private nextHandler: IDatasetHandler | null = null; + + /** + * Set the next handler in the chain + * @param handler The next handler + * @returns The next handler for method chaining + */ + setNext(handler: IDatasetHandler): IDatasetHandler { + this.nextHandler = handler; + return handler; + } + + /** + * Process the dataset and pass it to the next handler if needed + * @param dataset The dataset to process + * @returns The processed dataset or null if no handler can process it + */ + handle(dataset: IDataset): IDataset | null { + // Try to process the dataset with this handler + const result = this.process(dataset); + + // If this handler processed the dataset, return the result + if (result !== null) { + return result; + } + + // If this handler couldn't process the dataset and there's a next handler, + // pass the dataset to the next handler + if (this.nextHandler) { + return this.nextHandler.handle(dataset); + } + + // If no handler could process the dataset, return null + return null; + } + + /** + * Abstract method to be implemented by concrete handlers + * @param dataset The dataset to process + * @returns The processed dataset or null if the handler cannot process it + */ + protected abstract process(dataset: IDataset): IDataset | null; +} diff --git a/libs/map/dataset/src/model/dataset.visitors.ts b/libs/map/dataset/src/model/dataset.visitors.ts new file mode 100644 index 0000000..8f69129 --- /dev/null +++ b/libs/map/dataset/src/model/dataset.visitors.ts @@ -0,0 +1,505 @@ +import { IDataset } from '../interfaces/dataset.base'; +import { IDatasetVisitor } from '../interfaces/dataset.visitor'; + +/** + * Visitor that collects data from all datasets in the hierarchy + */ +export class DataCollectorVisitor implements IDatasetVisitor { + private collectedData: any[] = []; + + visitLeaf(dataset: IDataset): any { + this.collectedData.push({ + name: dataset.getName(), + data: dataset.getData(), + type: 'leaf', + }); + return this.collectedData; + } + + visitComposite(dataset: IDataset): any { + this.collectedData.push({ + name: dataset.getName(), + data: dataset.getData(), + type: 'composite', + childrenCount: dataset.getChildren?.()?.length || 0, + }); + + // Visit all children + if (dataset.getChildren) { + dataset.getChildren().forEach((child) => child.accept(this)); + } + + return this.collectedData; + } + + visitRoot(dataset: IDataset): any { + this.collectedData.push({ + name: dataset.getName(), + data: dataset.getData(), + type: 'root', + childrenCount: dataset.getChildren?.()?.length || 0, + }); + + // Visit all children + if (dataset.getChildren) { + dataset.getChildren().forEach((child) => child.accept(this)); + } + + return this.collectedData; + } + + getCollectedData(): any[] { + return this.collectedData; + } + + reset(): void { + this.collectedData = []; + } +} + +/** + * Visitor that finds a dataset by name + */ +export class DatasetFinderVisitor implements IDatasetVisitor { + private targetName: string; + private foundDataset: IDataset | null = null; + + constructor(targetName: string) { + this.targetName = targetName; + } + + visitLeaf(dataset: IDataset): any { + if (dataset.getName() === this.targetName) { + this.foundDataset = dataset; + } + return this.foundDataset; + } + + visitComposite(dataset: IDataset): any { + if (dataset.getName() === this.targetName) { + this.foundDataset = dataset; + return this.foundDataset; + } + + // If not found yet, search in children + if (dataset.getChildren && !this.foundDataset) { + for (const child of dataset.getChildren()) { + child.accept(this); + if (this.foundDataset) { + return this.foundDataset; + } + } + } + + return this.foundDataset; + } + + visitRoot(dataset: IDataset): any { + if (dataset.getName() === this.targetName) { + this.foundDataset = dataset; + return this.foundDataset; + } + + // If not found yet, search in children + if (dataset.getChildren && !this.foundDataset) { + for (const child of dataset.getChildren()) { + child.accept(this); + if (this.foundDataset) { + return this.foundDataset; + } + } + } + + return this.foundDataset; + } + + getFoundDataset(): IDataset | null { + return this.foundDataset; + } + + reset(targetName?: string): void { + this.foundDataset = null; + if (targetName) { + this.targetName = targetName; + } + } +} + +/** + * Visitor that traverses the dataset hierarchy and builds a path from root to a target dataset + */ +export class PathBuilderVisitor implements IDatasetVisitor { + private targetName: string; + private path: IDataset[] = []; + private found = false; + + constructor(targetName: string) { + this.targetName = targetName; + } + + visitLeaf(dataset: IDataset): any { + if (dataset.getName() === this.targetName) { + this.path.push(dataset); + this.found = true; + } + return this.path; + } + + visitComposite(dataset: IDataset): any { + if (dataset.getName() === this.targetName) { + this.path.push(dataset); + this.found = true; + return this.path; + } + + // If not found yet, search in children + if (dataset.getChildren && !this.found) { + for (const child of dataset.getChildren()) { + child.accept(this); + if (this.found) { + this.path.unshift(dataset); + return this.path; + } + } + } + + return this.path; + } + + visitRoot(dataset: IDataset): any { + if (dataset.getName() === this.targetName) { + this.path.push(dataset); + this.found = true; + return this.path; + } + + // If not found yet, search in children + if (dataset.getChildren && !this.found) { + for (const child of dataset.getChildren()) { + child.accept(this); + if (this.found) { + this.path.unshift(dataset); + return this.path; + } + } + } + + return this.path; + } + + getPath(): IDataset[] { + return this.path; + } + + isFound(): boolean { + return this.found; + } + + reset(targetName?: string): void { + this.path = []; + this.found = false; + if (targetName) { + this.targetName = targetName; + } + } +} + +/** + * Visitor that finds the root dataset by traversing up the parent chain + */ +export class RootFinderVisitor implements IDatasetVisitor { + private rootDataset: IDataset | null = null; + + visitLeaf(dataset: IDataset): any { + return this.findRoot(dataset); + } + + visitComposite(dataset: IDataset): any { + return this.findRoot(dataset); + } + + visitRoot(dataset: IDataset): any { + this.rootDataset = dataset; + return this.rootDataset; + } + + private findRoot(dataset: IDataset): IDataset | null { + let current: IDataset | undefined = dataset; + + while (current) { + const parent = current.getParent(); + if (!parent) { + this.rootDataset = current; + break; + } + current = parent; + } + + return this.rootDataset; + } + + getRootDataset(): IDataset | null { + return this.rootDataset; + } + + reset(): void { + this.rootDataset = null; + } +} + +/** + * Visitor that applies a function to all leaf nodes in the hierarchy + * This allows you to call one function on all leaf nodes with a single operation + */ +export class LeafFunctionVisitor implements IDatasetVisitor { + private leafFunction: (dataset: IDataset) => any; + private results: any[] = []; + + constructor(leafFunction: (dataset: IDataset) => any) { + this.leafFunction = leafFunction; + } + + visitLeaf(dataset: IDataset): any { + // Apply the function to the leaf node and store the result + const result = this.leafFunction(dataset); + this.results.push(result); + return this.results; + } + + visitComposite(dataset: IDataset): any { + // For composite nodes, just visit all children + if (dataset.getChildren) { + dataset.getChildren().forEach((child) => child.accept(this)); + } + return this.results; + } + + visitRoot(dataset: IDataset): any { + // For root nodes, just visit all children + if (dataset.getChildren) { + dataset.getChildren().forEach((child) => child.accept(this)); + } + return this.results; + } + + getResults(): any[] { + return this.results; + } + + reset(leafFunction?: (dataset: IDataset) => any): void { + this.results = []; + if (leafFunction) { + this.leafFunction = leafFunction; + } + } +} + +/** + * Helper function to apply a function to all leaf nodes in a dataset hierarchy + * @param rootDataset The root dataset to start from + * @param leafFunction The function to apply to each leaf node + * @returns An array of results from applying the function to each leaf node + */ +export function applyToAllLeaves( + rootDataset: IDataset, + leafFunction: (dataset: IDataset) => any +): any[] { + const visitor = new LeafFunctionVisitor(leafFunction); + rootDataset.accept(visitor); + return visitor.getResults(); +} + +/** + * Visitor that runs functions from one leaf node on all other leaf nodes + * This allows you to propagate operations from a single leaf to all other leaves + */ +export class LeafFunctionPropagatorVisitor implements IDatasetVisitor { + private sourceLeaf: IDataset | null = null; + private leafFunctions: ((dataset: IDataset) => any)[] = []; + private results: Map = new Map(); + + constructor(sourceLeafName: string) { + // Find the source leaf node + const finder = new DatasetFinderVisitor(sourceLeafName); + this.sourceLeaf = finder.getFoundDataset(); + } + + setSourceLeaf(sourceLeaf: IDataset): void { + this.sourceLeaf = sourceLeaf; + } + + addLeafFunction(leafFunction: (dataset: IDataset) => any): void { + this.leafFunctions.push(leafFunction); + } + + visitLeaf(dataset: IDataset): any { + // Skip the source leaf node + if (this.sourceLeaf && dataset === this.sourceLeaf) { + return this.results; + } + + // Apply all functions from the source leaf to this leaf + const leafResults: any[] = []; + for (const leafFunction of this.leafFunctions) { + const result = leafFunction(dataset); + leafResults.push(result); + } + + // Store the results for this leaf + this.results.set(dataset.getName(), leafResults); + return this.results; + } + + visitComposite(dataset: IDataset): any { + // For composite nodes, just visit all children + if (dataset.getChildren) { + dataset.getChildren().forEach((child) => child.accept(this)); + } + return this.results; + } + + visitRoot(dataset: IDataset): any { + // For root nodes, just visit all children + if (dataset.getChildren) { + dataset.getChildren().forEach((child) => child.accept(this)); + } + return this.results; + } + + getResults(): Map { + return this.results; + } + + reset(sourceLeaf?: IDataset): void { + this.results = new Map(); + if (sourceLeaf) { + this.sourceLeaf = sourceLeaf; + } + } +} + +/** + * Helper function to run functions from one leaf node on all other leaf nodes + * @param rootDataset The root dataset to start from + * @param sourceLeafName The name of the source leaf node + * @param leafFunctions The functions to apply to each leaf node + * @returns A map of leaf names to arrays of results from applying the functions + */ +export function propagateFromLeaf( + rootDataset: IDataset, + sourceLeafName: string, + leafFunctions: ((dataset: IDataset) => any)[] +): Map { + const visitor = new LeafFunctionPropagatorVisitor(sourceLeafName); + + // Add all functions to the visitor + for (const leafFunction of leafFunctions) { + visitor.addLeafFunction(leafFunction); + } + + // Accept the visitor on the root dataset + rootDataset.accept(visitor); + + return visitor.getResults(); +} + +/** + * Visitor that runs functions from one leaf node on all other leaf nodes without needing to know the root + * This allows you to propagate operations from a single leaf to all other leaves when you only have access to the leaf + */ +export class LeafToLeafFunctionVisitor implements IDatasetVisitor { + private sourceLeaf: IDataset; + private leafFunctions: ((dataset: IDataset) => any)[] = []; + private results: Map = new Map(); + private visitedLeaves: Set = new Set(); + + constructor(sourceLeaf: IDataset) { + this.sourceLeaf = sourceLeaf; + } + + addLeafFunction(leafFunction: (dataset: IDataset) => any): void { + this.leafFunctions.push(leafFunction); + } + + visitLeaf(dataset: IDataset): any { + // Skip the source leaf node and already visited leaves + if (dataset === this.sourceLeaf || this.visitedLeaves.has(dataset)) { + return this.results; + } + + // Mark this leaf as visited + this.visitedLeaves.add(dataset); + + // Apply all functions from the source leaf to this leaf + const leafResults: any[] = []; + for (const leafFunction of this.leafFunctions) { + const result = leafFunction(dataset); + leafResults.push(result); + } + + // Store the results for this leaf + this.results.set(dataset.getName(), leafResults); + return this.results; + } + + visitComposite(dataset: IDataset): any { + // For composite nodes, just visit all children + if (dataset.getChildren) { + dataset.getChildren().forEach((child) => child.accept(this)); + } + return this.results; + } + + visitRoot(dataset: IDataset): any { + // For root nodes, just visit all children + if (dataset.getChildren) { + dataset.getChildren().forEach((child) => child.accept(this)); + } + return this.results; + } + + getResults(): Map { + return this.results; + } + + reset(sourceLeaf?: IDataset): void { + this.results = new Map(); + this.visitedLeaves = new Set(); + if (sourceLeaf) { + this.sourceLeaf = sourceLeaf; + } + } +} + +/** + * Helper function to run functions from one leaf node on all other leaf nodes without needing to know the root + * @param sourceLeaf The source leaf node to start from + * @param leafFunctions The functions to apply to each leaf node + * @returns A map of leaf names to arrays of results from applying the functions + */ +export function runFromLeaf( + sourceLeaf: IDataset, + leafFunctions: ((dataset: IDataset) => any)[] +): Map { + // Find the root node by traversing up the parent chain + const rootFinder = new RootFinderVisitor(); + sourceLeaf.accept(rootFinder); + const rootDataset = rootFinder.getRootDataset(); + + if (!rootDataset) { + throw new Error('Could not find root dataset'); + } + + // Create a visitor to run functions on all other leaves + const visitor = new LeafToLeafFunctionVisitor(sourceLeaf); + + // Add all functions to the visitor + for (const leafFunction of leafFunctions) { + visitor.addLeafFunction(leafFunction); + } + + // Accept the visitor on the root dataset + rootDataset.accept(visitor); + + return visitor.getResults(); +} diff --git a/libs/map/dataset/src/model/part-list-view-ui.model.ts b/libs/map/dataset/src/model/part-list-view-ui.model.ts new file mode 100644 index 0000000..ec6b3ef --- /dev/null +++ b/libs/map/dataset/src/model/part-list-view-ui.model.ts @@ -0,0 +1,21 @@ +import { Color } from '@hungpvq/shared-map'; +import { + IGroupListViewUI, + IListViewUI, +} from '../interfaces/part-list-view-ui.interface'; +import { DatasetLeaf } from './dataset.base'; + +export class DatasetPartListViewUiComponent extends DatasetLeaf { + opacity = 1; + selected = false; + metadata: any; + color: Color = '#fff'; + config: { + disable_delete?: boolean; + disabled_opacity?: boolean; + component?: any; + } = {}; + index = 0; + group?: IGroupListViewUI; + show?: boolean; +} diff --git a/libs/map/dataset/src/parts/dataset.handlers.ts b/libs/map/dataset/src/parts/dataset.handlers.ts new file mode 100644 index 0000000..d1d4777 --- /dev/null +++ b/libs/map/dataset/src/parts/dataset.handlers.ts @@ -0,0 +1,95 @@ +import { IDataset } from '../interfaces/dataset.base'; +import { DatasetLeaf } from '../model/dataset.base'; +import { DatasetHandlerBase } from '../model/dataset.handler'; + +/** + * Handler for processing leaf datasets + * This handler only processes leaf datasets and ignores composite datasets + */ +export class LeafDatasetHandler extends DatasetHandlerBase { + protected process(dataset: IDataset): IDataset | null { + // Check if the dataset is a leaf dataset (doesn't have children) + if (!('getChildren' in dataset)) { + // Process the leaf dataset (in this example, we just return it) + // In a real application, you might transform the data or perform other operations + return dataset; + } + + // If it's not a leaf dataset, return null to pass it to the next handler + return null; + } +} + +/** + * Handler for processing composite datasets + * This handler only processes composite datasets and ignores leaf datasets + */ +export class CompositeDatasetHandler extends DatasetHandlerBase { + protected process(dataset: IDataset): IDataset | null { + // Check if the dataset is a composite dataset (has children) + if ('getChildren' in dataset) { + // Process the composite dataset (in this example, we just return it) + // In a real application, you might aggregate data from children or perform other operations + return dataset; + } + + // If it's not a composite dataset, return null to pass it to the next handler + return null; + } +} + +/** + * Handler for validating datasets + * This handler validates all datasets regardless of their type + */ +export class ValidationDatasetHandler extends DatasetHandlerBase { + protected process(dataset: IDataset): IDataset | null { + // Validate the dataset (in this example, we just check if it has a name) + if (dataset.getName() && dataset.getName().trim() !== '') { + // If the dataset is valid, return it to pass it to the next handler + return dataset; + } + + // If the dataset is invalid, throw an error + throw new Error('Invalid dataset: name is required'); + } +} + +/** + * Handler for transforming datasets + * This handler transforms all datasets regardless of their type + */ +export class TransformDatasetHandler extends DatasetHandlerBase { + private transformFn: (dataset: IDataset) => IDataset; + + constructor(transformFn: (dataset: IDataset) => IDataset) { + super(); + this.transformFn = transformFn; + } + + protected process(dataset: IDataset): IDataset | null { + // Transform the dataset using the provided function + return this.transformFn(dataset); + } +} + +/** + * Factory function to create a chain of dataset handlers + * @param handlers The handlers to include in the chain + * @returns The first handler in the chain + */ +export function createDatasetHandlerChain( + ...handlers: DatasetHandlerBase[] +): DatasetHandlerBase { + if (handlers.length === 0) { + throw new Error('At least one handler is required'); + } + + // Link the handlers in the order they were provided + for (let i = 0; i < handlers.length - 1; i++) { + handlers[i].setNext(handlers[i + 1]); + } + + // Return the first handler in the chain + return handlers[0]; +} From fc2cfd272133369dafbd88a0e0ff5adb27abad52 Mon Sep 17 00:00:00 2001 From: "hung.pv" Date: Thu, 10 Apr 2025 16:32:41 +0700 Subject: [PATCH 03/35] feat: init layer control --- .gitignore | 1 + .../src/views/GeojsonIo/geojsonIoLayer.vue | 2 - apps/demo-map/src/views/test.vue | 252 +++++--- libs/map/dataset/.eslintrc.json | 34 +- libs/map/dataset/src/index.ts | 6 +- .../dataset/src/interfaces/dataset.base.ts | 1 + .../map/dataset/src/interfaces/dataset.map.ts | 2 - .../dataset/src/interfaces/dataset.parts.ts | 36 ++ .../interfaces/part-list-view-ui.interface.ts | 25 - libs/map/dataset/src/model/dataset.base.ts | 39 +- .../map/dataset/src/model/dataset.visitors.ts | 611 +++++++----------- libs/map/dataset/src/model/index.ts | 7 + .../src/model/part-list-view-ui.model.ts | 20 +- .../src/model/part-mapbox-layer.model.ts | 87 +++ .../src/model/part-mapbox-source.model.ts | 42 ++ .../src/modules/LayerControl/LayerControl.vue | 225 +++++++ .../DraggableList/draggable-list-group.vue | 205 ++++++ .../DraggableList/draggable-list-item.vue | 74 +++ .../part/DraggableList/draggable-list.vue | 261 ++++++++ .../modules/LayerControl/part/LayerList.vue | 323 +++++++++ .../part/item/layer-item-icon.vue | 28 + .../part/item/layer-item-slider.vue | 145 +++++ .../LayerControl/part/item/layer-item.vue | 194 ++++++ .../LayerControl/part/item/menu/index.vue | 24 + .../part/item/menu/menu-divider.vue | 17 + .../LayerControl/part/item/menu/menu-item.vue | 24 + libs/map/dataset/src/modules/index.ts | 1 + libs/map/dataset/src/shims-vue.d.ts | 6 + libs/map/dataset/src/store/index.ts | 100 +++ libs/map/dataset/src/types/vue-icon.d.ts | 15 + libs/map/dataset/src/utils/check.ts | 29 + libs/map/dataset/tsconfig.json | 13 +- libs/map/dataset/tsconfig.lib.json | 25 +- 33 files changed, 2307 insertions(+), 567 deletions(-) create mode 100644 libs/map/dataset/src/interfaces/dataset.parts.ts delete mode 100644 libs/map/dataset/src/interfaces/part-list-view-ui.interface.ts create mode 100644 libs/map/dataset/src/model/index.ts create mode 100644 libs/map/dataset/src/model/part-mapbox-layer.model.ts create mode 100644 libs/map/dataset/src/model/part-mapbox-source.model.ts create mode 100644 libs/map/dataset/src/modules/LayerControl/LayerControl.vue create mode 100644 libs/map/dataset/src/modules/LayerControl/part/DraggableList/draggable-list-group.vue create mode 100644 libs/map/dataset/src/modules/LayerControl/part/DraggableList/draggable-list-item.vue create mode 100644 libs/map/dataset/src/modules/LayerControl/part/DraggableList/draggable-list.vue create mode 100644 libs/map/dataset/src/modules/LayerControl/part/LayerList.vue create mode 100644 libs/map/dataset/src/modules/LayerControl/part/item/layer-item-icon.vue create mode 100644 libs/map/dataset/src/modules/LayerControl/part/item/layer-item-slider.vue create mode 100644 libs/map/dataset/src/modules/LayerControl/part/item/layer-item.vue create mode 100644 libs/map/dataset/src/modules/LayerControl/part/item/menu/index.vue create mode 100644 libs/map/dataset/src/modules/LayerControl/part/item/menu/menu-divider.vue create mode 100644 libs/map/dataset/src/modules/LayerControl/part/item/menu/menu-item.vue create mode 100644 libs/map/dataset/src/modules/index.ts create mode 100644 libs/map/dataset/src/shims-vue.d.ts create mode 100644 libs/map/dataset/src/store/index.ts create mode 100644 libs/map/dataset/src/types/vue-icon.d.ts create mode 100644 libs/map/dataset/src/utils/check.ts diff --git a/.gitignore b/.gitignore index bcd38ba..db07dd4 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,4 @@ Thumbs.db docs/.vitepress/dist docs/.vitepress/cache .npmrc +*.mjs diff --git a/apps/demo-map/src/views/GeojsonIo/geojsonIoLayer.vue b/apps/demo-map/src/views/GeojsonIo/geojsonIoLayer.vue index 6299d17..a4a6eaf 100644 --- a/apps/demo-map/src/views/GeojsonIo/geojsonIoLayer.vue +++ b/apps/demo-map/src/views/GeojsonIo/geojsonIoLayer.vue @@ -70,7 +70,6 @@ const layer = shallowRef( geojson: geojson.value, }) ); -console.log(layer.value.getView('source')); const { callMap, moduleContainerProps } = useMap( props, (map: MapSimple) => { @@ -81,7 +80,6 @@ const { callMap, moduleContainerProps } = useMap( } ); function onChange(value: string) { - console.log(value); const source = layer.value.getView('source') as GeojsonSource; value = JSON.parse(value); if (!geojsonValidation.valid(value)) { diff --git a/apps/demo-map/src/views/test.vue b/apps/demo-map/src/views/test.vue index e4225b4..9b19e4c 100644 --- a/apps/demo-map/src/views/test.vue +++ b/apps/demo-map/src/views/test.vue @@ -1,113 +1,153 @@ - - + -const updateFunction2 = (dataset) => { - // Another update function - const data = dataset.getData(); - data.anotherProperty = 42; - dataset.setData(data); - return ['Updated with function 2', data]; -}; + -// Run these functions from this leaf on all other leaves -const results = runFromLeaf(leafNode, [updateFunction1, updateFunction2]); + +body, +html, +#root { + height: 100%; +} + diff --git a/libs/map/dataset/.eslintrc.json b/libs/map/dataset/.eslintrc.json index 7b25f23..04e3c00 100644 --- a/libs/map/dataset/.eslintrc.json +++ b/libs/map/dataset/.eslintrc.json @@ -1,27 +1,25 @@ { - "extends": ["../../../.eslintrc.base.json"], + "extends": [ + "plugin:vue/vue3-essential", + "eslint:recommended", + "@vue/eslint-config-typescript", + "@vue/eslint-config-prettier/skip-formatting", + "../../../.eslintrc.base.json" + ], "ignorePatterns": ["!**/*"], "overrides": [ { - "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], - "rules": {} - }, - { - "files": ["*.ts", "*.tsx"], - "rules": {} - }, - { - "files": ["*.js", "*.jsx"], - "rules": {} - }, - { - "files": ["*.json"], - "parser": "jsonc-eslint-parser", + "files": ["*.ts", "*.tsx", "*.js", "*.jsx", "*.vue"], "rules": { - "@nx/dependency-checks": [ - "error", + "@typescript-eslint/no-explicit-any": "off", + "vue/multi-word-component-names": "off", + "@typescript-eslint/ban-types": [ + "warn", { - "ignoredFiles": ["{projectRoot}/vite.config.{js,ts,mjs,mts}"] + "extendDefaults": true, + "types": { + "{}": false + } } ] } diff --git a/libs/map/dataset/src/index.ts b/libs/map/dataset/src/index.ts index a410b18..8a91626 100644 --- a/libs/map/dataset/src/index.ts +++ b/libs/map/dataset/src/index.ts @@ -1,7 +1,7 @@ export * from './interfaces/dataset.base'; export * from './interfaces/dataset.handler'; export * from './interfaces/dataset.visitor'; -export * from './model/dataset.base'; -export * from './model/dataset.handler'; -export * from './model/dataset.visitors'; +export * from './model'; +export * from './modules'; export * from './parts/dataset.handlers'; +export * from './store'; diff --git a/libs/map/dataset/src/interfaces/dataset.base.ts b/libs/map/dataset/src/interfaces/dataset.base.ts index afeaa40..56e5a4d 100644 --- a/libs/map/dataset/src/interfaces/dataset.base.ts +++ b/libs/map/dataset/src/interfaces/dataset.base.ts @@ -5,6 +5,7 @@ import { IDatasetVisitor } from './dataset.visitor'; * This interface defines the common operations for both leaf and composite nodes */ export interface IDataset extends IDatasetVisit { + get type(): string; get id(): string; /** * Get the name of the dataset diff --git a/libs/map/dataset/src/interfaces/dataset.map.ts b/libs/map/dataset/src/interfaces/dataset.map.ts index a164c56..79307fa 100644 --- a/libs/map/dataset/src/interfaces/dataset.map.ts +++ b/libs/map/dataset/src/interfaces/dataset.map.ts @@ -3,6 +3,4 @@ import type { MapSimple } from '@hungpvq/shared-map'; export interface IDatasetMap { addToMap(map: MapSimple, beforeId?: string): void; removeFromMap(map: MapSimple): void; - getExecutionOrder?(functionName: string): number; - setExecutionOrder?(functionName: string, order: number): void; } diff --git a/libs/map/dataset/src/interfaces/dataset.parts.ts b/libs/map/dataset/src/interfaces/dataset.parts.ts new file mode 100644 index 0000000..3d7f5e9 --- /dev/null +++ b/libs/map/dataset/src/interfaces/dataset.parts.ts @@ -0,0 +1,36 @@ +import { Color, MapSimple } from '@hungpvq/shared-map'; +import { AnySourceData } from 'mapbox-gl'; +import { IDatasetMap } from './dataset.map'; + +// === list === +export type IGroupListViewUI = + | string + | { + name: string; + id: string; + children?: T[]; + }; +export type IListViewUI = { + getName: () => string; + opacity: number; + selected: boolean; + metadata: any; + color?: Color; + config: { + disable_delete?: boolean; + disabled_opacity?: boolean; + component?: any; + }; + index: number; + group?: IGroupListViewUI; + show?: boolean; +}; +export type IMapboxSourceView = IDatasetMap & { + getMapboxSource: () => AnySourceData; +}; +export type IMapboxLayerView = IDatasetMap & { + getBeforeId(): string; + setOpacity(map: MapSimple, opacity: number): void; + toggleShow(map: MapSimple, show?: boolean): void; + moveLayer(map: MapSimple, beforeId: string): void; +}; diff --git a/libs/map/dataset/src/interfaces/part-list-view-ui.interface.ts b/libs/map/dataset/src/interfaces/part-list-view-ui.interface.ts deleted file mode 100644 index 3bb0e8c..0000000 --- a/libs/map/dataset/src/interfaces/part-list-view-ui.interface.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Color } from '@hungpvq/shared-map'; - -// === list === -export type IGroupListViewUI = - | string - | { - name: string; - id: string; - children?: T[]; - }; -export type IListViewUI = { - name?: string; - opacity: number; - selected: boolean; - metadata: any; - color: Color; - config: { - disable_delete?: boolean; - disabled_opacity?: boolean; - component?: any; - }; - index: number; - group?: IGroupListViewUI; - show: any; -}; diff --git a/libs/map/dataset/src/model/dataset.base.ts b/libs/map/dataset/src/model/dataset.base.ts index 0e1f4b8..24d67f9 100644 --- a/libs/map/dataset/src/model/dataset.base.ts +++ b/libs/map/dataset/src/model/dataset.base.ts @@ -6,13 +6,12 @@ import { Base } from './base'; * The base DatasetComponent class declares common operations for both simple and * complex objects of a composition. */ -abstract class DatasetComponent extends Base implements IDataset { +abstract class DatasetComponent extends Base implements IDataset { protected parent?: DatasetComponent; protected name: string; - protected data: any; - protected executionOrders: Map = new Map(); - - constructor(name: string, data?: any) { + protected data?: T; + abstract type: string; + constructor(name: string, data?: T) { super(); this.name = name; this.data = data; @@ -57,32 +56,17 @@ abstract class DatasetComponent extends Base implements IDataset { setData(data: any) { this.data = data; } - - /** - * Get the execution order for a specific function - * Lower values are executed first - * @param functionName The name of the function - * @returns The execution order value - */ - getExecutionOrder(functionName: string): number { - return this.executionOrders.get(functionName) ?? 5; // Default order is 5 - } - - /** - * Set the execution order for a specific function - * @param functionName The name of the function - * @param order The execution order value (lower values are executed first) - */ - setExecutionOrder(functionName: string, order: number): void { - this.executionOrders.set(functionName, order); - } } /** * Leaf node in the Dataset Composite pattern * Represents a single dataset without children */ -export class DatasetLeaf extends DatasetComponent { - constructor(name: string, data: any) { +export class DatasetLeaf extends DatasetComponent { + get type(): string { + return 'leaf'; + } + + constructor(name: string, data?: T) { super(name, data); } @@ -101,6 +85,9 @@ export class DatasetLeaf extends DatasetComponent { * Represents a dataset that can contain other datasets */ export class DatasetComposite extends DatasetComponent { + get type(): string { + return 'composite'; + } private children: IDataset[] = []; constructor(name: string) { diff --git a/libs/map/dataset/src/model/dataset.visitors.ts b/libs/map/dataset/src/model/dataset.visitors.ts index 8f69129..f4f5c10 100644 --- a/libs/map/dataset/src/model/dataset.visitors.ts +++ b/libs/map/dataset/src/model/dataset.visitors.ts @@ -2,212 +2,39 @@ import { IDataset } from '../interfaces/dataset.base'; import { IDatasetVisitor } from '../interfaces/dataset.visitor'; /** - * Visitor that collects data from all datasets in the hierarchy + * Base visitor class that provides common functionality for all dataset visitors. + * This is an abstract class that all other visitors inherit from. + * + * Usage: + * To create a new visitor, extend this class and implement: + * - visitLeaf(dataset: IDataset): any + * - visitComposite(dataset: IDataset): any + * - visitRoot(dataset: IDataset): any */ -export class DataCollectorVisitor implements IDatasetVisitor { - private collectedData: any[] = []; - - visitLeaf(dataset: IDataset): any { - this.collectedData.push({ - name: dataset.getName(), - data: dataset.getData(), - type: 'leaf', - }); - return this.collectedData; - } - - visitComposite(dataset: IDataset): any { - this.collectedData.push({ - name: dataset.getName(), - data: dataset.getData(), - type: 'composite', - childrenCount: dataset.getChildren?.()?.length || 0, - }); - - // Visit all children - if (dataset.getChildren) { - dataset.getChildren().forEach((child) => child.accept(this)); - } - - return this.collectedData; - } - - visitRoot(dataset: IDataset): any { - this.collectedData.push({ - name: dataset.getName(), - data: dataset.getData(), - type: 'root', - childrenCount: dataset.getChildren?.()?.length || 0, - }); - - // Visit all children +abstract class BaseDatasetVisitor implements IDatasetVisitor { + protected visitChildren(dataset: IDataset): void { if (dataset.getChildren) { dataset.getChildren().forEach((child) => child.accept(this)); } - - return this.collectedData; - } - - getCollectedData(): any[] { - return this.collectedData; - } - - reset(): void { - this.collectedData = []; - } -} - -/** - * Visitor that finds a dataset by name - */ -export class DatasetFinderVisitor implements IDatasetVisitor { - private targetName: string; - private foundDataset: IDataset | null = null; - - constructor(targetName: string) { - this.targetName = targetName; - } - - visitLeaf(dataset: IDataset): any { - if (dataset.getName() === this.targetName) { - this.foundDataset = dataset; - } - return this.foundDataset; - } - - visitComposite(dataset: IDataset): any { - if (dataset.getName() === this.targetName) { - this.foundDataset = dataset; - return this.foundDataset; - } - - // If not found yet, search in children - if (dataset.getChildren && !this.foundDataset) { - for (const child of dataset.getChildren()) { - child.accept(this); - if (this.foundDataset) { - return this.foundDataset; - } - } - } - - return this.foundDataset; - } - - visitRoot(dataset: IDataset): any { - if (dataset.getName() === this.targetName) { - this.foundDataset = dataset; - return this.foundDataset; - } - - // If not found yet, search in children - if (dataset.getChildren && !this.foundDataset) { - for (const child of dataset.getChildren()) { - child.accept(this); - if (this.foundDataset) { - return this.foundDataset; - } - } - } - - return this.foundDataset; - } - - getFoundDataset(): IDataset | null { - return this.foundDataset; } - reset(targetName?: string): void { - this.foundDataset = null; - if (targetName) { - this.targetName = targetName; - } - } + abstract visitLeaf(dataset: IDataset): any; + abstract visitComposite(dataset: IDataset): any; + abstract visitRoot(dataset: IDataset): any; } /** - * Visitor that traverses the dataset hierarchy and builds a path from root to a target dataset + * Visitor that finds the root dataset by traversing up the parent chain. + * + * Usage: + * const rootFinder = new RootFinderVisitor(); + * anyDataset.accept(rootFinder); + * const root = rootFinder.getRootDataset(); + * + * // Reset and find root of different dataset + * rootFinder.reset(); */ -export class PathBuilderVisitor implements IDatasetVisitor { - private targetName: string; - private path: IDataset[] = []; - private found = false; - - constructor(targetName: string) { - this.targetName = targetName; - } - - visitLeaf(dataset: IDataset): any { - if (dataset.getName() === this.targetName) { - this.path.push(dataset); - this.found = true; - } - return this.path; - } - - visitComposite(dataset: IDataset): any { - if (dataset.getName() === this.targetName) { - this.path.push(dataset); - this.found = true; - return this.path; - } - - // If not found yet, search in children - if (dataset.getChildren && !this.found) { - for (const child of dataset.getChildren()) { - child.accept(this); - if (this.found) { - this.path.unshift(dataset); - return this.path; - } - } - } - - return this.path; - } - - visitRoot(dataset: IDataset): any { - if (dataset.getName() === this.targetName) { - this.path.push(dataset); - this.found = true; - return this.path; - } - - // If not found yet, search in children - if (dataset.getChildren && !this.found) { - for (const child of dataset.getChildren()) { - child.accept(this); - if (this.found) { - this.path.unshift(dataset); - return this.path; - } - } - } - - return this.path; - } - - getPath(): IDataset[] { - return this.path; - } - - isFound(): boolean { - return this.found; - } - - reset(targetName?: string): void { - this.path = []; - this.found = false; - if (targetName) { - this.targetName = targetName; - } - } -} - -/** - * Visitor that finds the root dataset by traversing up the parent chain - */ -export class RootFinderVisitor implements IDatasetVisitor { +export class RootFinderVisitor extends BaseDatasetVisitor { private rootDataset: IDataset | null = null; visitLeaf(dataset: IDataset): any { @@ -225,7 +52,6 @@ export class RootFinderVisitor implements IDatasetVisitor { private findRoot(dataset: IDataset): IDataset | null { let current: IDataset | undefined = dataset; - while (current) { const parent = current.getParent(); if (!parent) { @@ -234,7 +60,6 @@ export class RootFinderVisitor implements IDatasetVisitor { } current = parent; } - return this.rootDataset; } @@ -248,258 +73,310 @@ export class RootFinderVisitor implements IDatasetVisitor { } /** - * Visitor that applies a function to all leaf nodes in the hierarchy - * This allows you to call one function on all leaf nodes with a single operation + * Visitor that finds the first leaf with specified type from a different leaf. + * + * Usage: + * const leafFinder = new TypeLeafFinderVisitor(sourceLeaf, 'targetType'); + * rootDataset.accept(leafFinder); + * const foundLeaf = leafFinder.getFoundLeaf(); + * + * // Reset and search for different type + * leafFinder.reset(sourceLeaf, 'newType'); */ -export class LeafFunctionVisitor implements IDatasetVisitor { - private leafFunction: (dataset: IDataset) => any; - private results: any[] = []; +export class TypeLeafFinderVisitor extends BaseDatasetVisitor { + private sourceLeaf: IDataset | null = null; + private targetType: string; + private foundLeaf: IDataset | null = null; - constructor(leafFunction: (dataset: IDataset) => any) { - this.leafFunction = leafFunction; + constructor(sourceLeaf: IDataset, targetType: string) { + super(); + this.sourceLeaf = sourceLeaf; + this.targetType = targetType; } visitLeaf(dataset: IDataset): any { - // Apply the function to the leaf node and store the result - const result = this.leafFunction(dataset); - this.results.push(result); - return this.results; + if (this.sourceLeaf && dataset === this.sourceLeaf) { + return this.foundLeaf; + } + + if (dataset.type === this.targetType && !this.foundLeaf) { + this.foundLeaf = dataset; + } + return this.foundLeaf; } visitComposite(dataset: IDataset): any { - // For composite nodes, just visit all children - if (dataset.getChildren) { - dataset.getChildren().forEach((child) => child.accept(this)); + if (!this.foundLeaf) { + this.visitChildren(dataset); } - return this.results; + return this.foundLeaf; } visitRoot(dataset: IDataset): any { - // For root nodes, just visit all children - if (dataset.getChildren) { - dataset.getChildren().forEach((child) => child.accept(this)); - } - return this.results; + return this.visitComposite(dataset); } - getResults(): any[] { - return this.results; + getFoundLeaf(): IDataset | null { + return this.foundLeaf; } - reset(leafFunction?: (dataset: IDataset) => any): void { - this.results = []; - if (leafFunction) { - this.leafFunction = leafFunction; + reset(sourceLeaf?: IDataset, targetType?: string): void { + this.foundLeaf = null; + if (sourceLeaf) { + this.sourceLeaf = sourceLeaf; + } + if (targetType) { + this.targetType = targetType; } } } /** - * Helper function to apply a function to all leaf nodes in a dataset hierarchy - * @param rootDataset The root dataset to start from - * @param leafFunction The function to apply to each leaf node - * @returns An array of results from applying the function to each leaf node + * Helper function to find the first leaf with specified type from a different leaf. + * + * Usage: + * const foundLeaf = findFirstLeafByType(sourceLeaf, 'targetType'); + * if (foundLeaf) { + * // Use the found leaf + * } */ -export function applyToAllLeaves( - rootDataset: IDataset, - leafFunction: (dataset: IDataset) => any -): any[] { - const visitor = new LeafFunctionVisitor(leafFunction); +export function findFirstLeafByType( + sourceLeaf: IDataset, + targetType: string +): IDataset | null { + const rootFinder = new RootFinderVisitor(); + sourceLeaf.accept(rootFinder); + const rootDataset = rootFinder.getRootDataset(); + + if (!rootDataset) { + throw new Error('Could not find root dataset'); + } + + const visitor = new TypeLeafFinderVisitor(sourceLeaf, targetType); rootDataset.accept(visitor); - return visitor.getResults(); + + return visitor.getFoundLeaf(); } +// ===== Type Collection and Function Application Visitors ===== /** - * Visitor that runs functions from one leaf node on all other leaf nodes - * This allows you to propagate operations from a single leaf to all other leaves + * This section contains visitors for collecting datasets by type and applying functions to them. + * + * Usage examples: + * + * // Collect all components of a specific type + * const collector = new TypeFunctionVisitor((dataset) => dataset.type === 'targetType'); + * rootDataset.accept(collector); + * const foundComponents = collector.getFoundComponents(); + * + * // Apply functions to collected components + * collector.addFunction((dataset) => dataset.getData()); + * collector.addFunction((dataset) => dataset.getName()); + * const results = collector.getResults(); + * + * // Reset and reuse with different type check and functions + * collector.reset( + * (dataset) => dataset.type === 'newType', + * [(dataset) => dataset.getData()] + * ); */ -export class LeafFunctionPropagatorVisitor implements IDatasetVisitor { - private sourceLeaf: IDataset | null = null; - private leafFunctions: ((dataset: IDataset) => any)[] = []; - private results: Map = new Map(); - constructor(sourceLeafName: string) { - // Find the source leaf node - const finder = new DatasetFinderVisitor(sourceLeafName); - this.sourceLeaf = finder.getFoundDataset(); - } +/** + * Visitor that collects components by type and applies functions to them. + * + * Usage: + * // Basic type collection + * const visitor = new TypeFunctionVisitor((dataset) => dataset.type === 'targetType'); + * rootDataset.accept(visitor); + * const components = visitor.getFoundComponents(); + * + * // Add functions to apply + * visitor.addFunction((dataset) => dataset.getData()); + * visitor.addFunction((dataset) => dataset.getName()); + * const results = visitor.getResults(); + * // results is a Map where key is component name and value is array of function results + * + * // Reset with new type check and functions + * visitor.reset( + * (dataset) => dataset.type === 'newType', + * [(dataset) => dataset.getData()] + * ); + */ +export class TypeFunctionVisitor extends BaseDatasetVisitor { + private typeCheckFunction: (dataset: IDataset) => boolean; + private functions: ((dataset: IDataset) => any)[] = []; + private foundComponents: IDataset[] = []; + private results: Map = new Map(); - setSourceLeaf(sourceLeaf: IDataset): void { - this.sourceLeaf = sourceLeaf; + constructor( + typeCheckFunction: (dataset: IDataset) => boolean, + functions: ((dataset: IDataset) => any)[] = [] + ) { + super(); + this.typeCheckFunction = typeCheckFunction; + this.functions = functions; } - addLeafFunction(leafFunction: (dataset: IDataset) => any): void { - this.leafFunctions.push(leafFunction); + addFunction(func: (dataset: IDataset) => any): void { + this.functions.push(func); } visitLeaf(dataset: IDataset): any { - // Skip the source leaf node - if (this.sourceLeaf && dataset === this.sourceLeaf) { - return this.results; - } - - // Apply all functions from the source leaf to this leaf - const leafResults: any[] = []; - for (const leafFunction of this.leafFunctions) { - const result = leafFunction(dataset); - leafResults.push(result); + if (this.typeCheckFunction(dataset)) { + this.foundComponents.push(dataset); + if (this.functions.length > 0) { + const functionResults = this.functions.map((fn) => fn(dataset)); + this.results.set(dataset.getName(), functionResults); + } } - - // Store the results for this leaf - this.results.set(dataset.getName(), leafResults); - return this.results; + return this.foundComponents; } visitComposite(dataset: IDataset): any { - // For composite nodes, just visit all children - if (dataset.getChildren) { - dataset.getChildren().forEach((child) => child.accept(this)); + if (this.typeCheckFunction(dataset)) { + this.foundComponents.push(dataset); + if (this.functions.length > 0) { + const functionResults = this.functions.map((fn) => fn(dataset)); + this.results.set(dataset.getName(), functionResults); + } } - return this.results; + this.visitChildren(dataset); + return this.foundComponents; } visitRoot(dataset: IDataset): any { - // For root nodes, just visit all children - if (dataset.getChildren) { - dataset.getChildren().forEach((child) => child.accept(this)); - } - return this.results; + return this.visitComposite(dataset); + } + + getFoundComponents(): IDataset[] { + return this.foundComponents; } getResults(): Map { return this.results; } - reset(sourceLeaf?: IDataset): void { + reset( + typeCheckFunction?: (dataset: IDataset) => boolean, + functions?: ((dataset: IDataset) => any)[] + ): void { + this.foundComponents = []; this.results = new Map(); - if (sourceLeaf) { - this.sourceLeaf = sourceLeaf; + if (typeCheckFunction) { + this.typeCheckFunction = typeCheckFunction; + } + if (functions) { + this.functions = functions; } } } -/** - * Helper function to run functions from one leaf node on all other leaf nodes - * @param rootDataset The root dataset to start from - * @param sourceLeafName The name of the source leaf node - * @param leafFunctions The functions to apply to each leaf node - * @returns A map of leaf names to arrays of results from applying the functions - */ -export function propagateFromLeaf( +export function findAllComponentsByCheck( rootDataset: IDataset, - sourceLeafName: string, - leafFunctions: ((dataset: IDataset) => any)[] + typeCheckFunction: (dataset: IDataset) => boolean, + functions: ((dataset: IDataset) => any)[] = [] ): Map { - const visitor = new LeafFunctionPropagatorVisitor(sourceLeafName); - - // Add all functions to the visitor - for (const leafFunction of leafFunctions) { - visitor.addLeafFunction(leafFunction); - } - - // Accept the visitor on the root dataset + const visitor = new TypeFunctionVisitor(typeCheckFunction, functions); rootDataset.accept(visitor); + return visitor.getResults(); +} +export function applyToAllLeaves( + rootDataset: IDataset, + functions: ((dataset: IDataset) => any)[] = [] +): Map { + const visitor = new TypeFunctionVisitor( + (dataset) => !dataset.isComposite(), + functions + ); + rootDataset.accept(visitor); return visitor.getResults(); } +// ===== Type Collection Visitors ===== /** - * Visitor that runs functions from one leaf node on all other leaf nodes without needing to know the root - * This allows you to propagate operations from a single leaf to all other leaves when you only have access to the leaf + * This section contains visitors for collecting datasets by type. + * + * Usage examples: + * + * // Collect all components of a specific type + * const collector = new TypeCollectorVisitor((dataset) => dataset.type === 'targetType'); + * rootDataset.accept(collector); + * const foundComponents = collector.getFoundComponents(); + * + * // Or use the helper function + * const components = findAllComponentsByType(rootDataset, 'targetType'); + * + * // Reset and reuse with different type check + * collector.reset((dataset) => dataset.type === 'newType'); */ -export class LeafToLeafFunctionVisitor implements IDatasetVisitor { - private sourceLeaf: IDataset; - private leafFunctions: ((dataset: IDataset) => any)[] = []; - private results: Map = new Map(); - private visitedLeaves: Set = new Set(); - constructor(sourceLeaf: IDataset) { - this.sourceLeaf = sourceLeaf; - } +/** + * Visitor that collects all components of a specific type. + * + * Usage: + * const collector = new TypeCollectorVisitor((dataset) => dataset.type === 'targetType'); + * rootDataset.accept(collector); + * const components = collector.getFoundComponents(); + * + * // Reset and collect different type + * collector.reset((dataset) => dataset.type === 'newType'); + */ +export class TypeCollectorVisitor extends BaseDatasetVisitor { + private typeCheckFunction: (dataset: IDataset) => boolean; + private foundComponents: IDataset[] = []; - addLeafFunction(leafFunction: (dataset: IDataset) => any): void { - this.leafFunctions.push(leafFunction); + constructor(typeCheckFunction: (dataset: IDataset) => boolean) { + super(); + this.typeCheckFunction = typeCheckFunction; } visitLeaf(dataset: IDataset): any { - // Skip the source leaf node and already visited leaves - if (dataset === this.sourceLeaf || this.visitedLeaves.has(dataset)) { - return this.results; - } - - // Mark this leaf as visited - this.visitedLeaves.add(dataset); - - // Apply all functions from the source leaf to this leaf - const leafResults: any[] = []; - for (const leafFunction of this.leafFunctions) { - const result = leafFunction(dataset); - leafResults.push(result); + if (this.typeCheckFunction(dataset)) { + this.foundComponents.push(dataset); } - - // Store the results for this leaf - this.results.set(dataset.getName(), leafResults); - return this.results; + return this.foundComponents; } visitComposite(dataset: IDataset): any { - // For composite nodes, just visit all children - if (dataset.getChildren) { - dataset.getChildren().forEach((child) => child.accept(this)); + if (this.typeCheckFunction(dataset)) { + this.foundComponents.push(dataset); } - return this.results; + this.visitChildren(dataset); + return this.foundComponents; } visitRoot(dataset: IDataset): any { - // For root nodes, just visit all children - if (dataset.getChildren) { - dataset.getChildren().forEach((child) => child.accept(this)); - } - return this.results; + return this.visitComposite(dataset); } - getResults(): Map { - return this.results; + getFoundComponents(): IDataset[] { + return this.foundComponents; } - reset(sourceLeaf?: IDataset): void { - this.results = new Map(); - this.visitedLeaves = new Set(); - if (sourceLeaf) { - this.sourceLeaf = sourceLeaf; + reset(typeCheckFunction?: (dataset: IDataset) => boolean): void { + this.foundComponents = []; + if (typeCheckFunction) { + this.typeCheckFunction = typeCheckFunction; } } } - /** - * Helper function to run functions from one leaf node on all other leaf nodes without needing to know the root - * @param sourceLeaf The source leaf node to start from - * @param leafFunctions The functions to apply to each leaf node - * @returns A map of leaf names to arrays of results from applying the functions + * Helper function to find all components of a specific type. + * + * Usage: + * const components = findAllComponentsByType(rootDataset, 'targetType'); + * components.forEach(component => { + * console.log(component.getName()); + * }); */ -export function runFromLeaf( - sourceLeaf: IDataset, - leafFunctions: ((dataset: IDataset) => any)[] -): Map { - // Find the root node by traversing up the parent chain - const rootFinder = new RootFinderVisitor(); - sourceLeaf.accept(rootFinder); - const rootDataset = rootFinder.getRootDataset(); - - if (!rootDataset) { - throw new Error('Could not find root dataset'); - } - - // Create a visitor to run functions on all other leaves - const visitor = new LeafToLeafFunctionVisitor(sourceLeaf); - - // Add all functions to the visitor - for (const leafFunction of leafFunctions) { - visitor.addLeafFunction(leafFunction); - } - - // Accept the visitor on the root dataset +export function findAllComponentsByType( + rootDataset: IDataset, + targetType: string +): IDataset[] { + const visitor = new TypeCollectorVisitor( + (dataset) => dataset.type === targetType + ); rootDataset.accept(visitor); - - return visitor.getResults(); + return visitor.getFoundComponents(); } diff --git a/libs/map/dataset/src/model/index.ts b/libs/map/dataset/src/model/index.ts new file mode 100644 index 0000000..617e36a --- /dev/null +++ b/libs/map/dataset/src/model/index.ts @@ -0,0 +1,7 @@ +export * from './dataset.base'; +export * from './dataset.handler'; +export * from './dataset.visitors'; + +export * from './part-list-view-ui.model'; +export * from './part-mapbox-layer.model'; +export * from './part-mapbox-source.model'; diff --git a/libs/map/dataset/src/model/part-list-view-ui.model.ts b/libs/map/dataset/src/model/part-list-view-ui.model.ts index ec6b3ef..06a9bfd 100644 --- a/libs/map/dataset/src/model/part-list-view-ui.model.ts +++ b/libs/map/dataset/src/model/part-list-view-ui.model.ts @@ -1,21 +1,25 @@ import { Color } from '@hungpvq/shared-map'; -import { - IGroupListViewUI, - IListViewUI, -} from '../interfaces/part-list-view-ui.interface'; +import { IGroupListViewUI, IListViewUI } from '../interfaces/dataset.parts'; import { DatasetLeaf } from './dataset.base'; -export class DatasetPartListViewUiComponent extends DatasetLeaf { +export class DatasetPartListViewUiComponent + extends DatasetLeaf + implements IListViewUI +{ + override get type(): string { + return 'list'; + } + opacity = 1; selected = false; metadata: any; - color: Color = '#fff'; + color?: Color; config: { disable_delete?: boolean; disabled_opacity?: boolean; component?: any; - } = {}; + } = { disable_delete: false, disabled_opacity: false }; index = 0; group?: IGroupListViewUI; - show?: boolean; + show = true; } diff --git a/libs/map/dataset/src/model/part-mapbox-layer.model.ts b/libs/map/dataset/src/model/part-mapbox-layer.model.ts new file mode 100644 index 0000000..545e52c --- /dev/null +++ b/libs/map/dataset/src/model/part-mapbox-layer.model.ts @@ -0,0 +1,87 @@ +import { MapSimple } from '@hungpvq/shared-map'; +import { IMapboxLayerView } from '../interfaces/dataset.parts'; +import { DatasetLeaf } from './dataset.base'; +import { findFirstLeafByType } from './dataset.visitors'; + +export abstract class DatasetPartMapboxLayerComponent + extends DatasetLeaf + implements IMapboxLayerView +{ + override get type(): string { + return 'layer'; + } + abstract getBeforeId(): string; + abstract addToMap(map: MapSimple, beforeId?: string): void; + abstract removeFromMap(map: MapSimple): void; + abstract setOpacity(map: MapSimple, opacity: number): void; + abstract toggleShow(map: MapSimple, show: boolean): void; + abstract moveLayer(map: MapSimple, beforeId: string): void; +} +export class MultiMapboxLayerComponent + extends DatasetLeaf + implements IMapboxLayerView +{ + constructor(name: string, data: any[]) { + super(name, data); + + this.data.forEach((layer: any, id: number) => { + if (!layer.id) { + layer.id = `${this.id}-${id}`; + } + }); + } + get layers() { + return (this.data || []) as any[]; + } + getBeforeId() { + return this.layers[0].id; + } + addToMap(map: MapSimple, beforeId: string): void { + const source = findFirstLeafByType(this, 'source'); + this.layers.forEach((layer) => { + if (!map.getLayer(layer.id)) { + if (!layer.source && source && source.id) { + layer.source = source.id; + } + map.addLayer(layer, beforeId); + } + }); + } + removeFromMap(map: MapSimple): void { + this.layers.forEach((layer) => { + if (map.getLayer(layer.id)) { + map.removeLayer(layer.id); + } + }); + } + + moveLayer(map: MapSimple, beforeId: string): void { + this.layers.forEach((layer) => { + if (map.getLayer(layer.id)) { + map.moveLayer(layer.id, beforeId); + } + }); + } + + toggleShow(map: MapSimple, show: boolean): void { + this.layers.forEach((layer) => { + if (map.getLayer(layer.id)) { + map.setLayoutProperty( + layer.id, + 'visibility', + show ? 'visible' : 'none' + ); + } + }); + } + setOpacity(map: MapSimple, opacity: number): void { + this.layers.forEach((layer) => { + if (map.getLayer(layer.id)) { + const keyOpacity = + layer.type == 'symbol' ? `icon-opacity` : `${layer.type}-opacity`; + + map.setPaintProperty(layer.id, keyOpacity, opacity); + } + }); + } +} diff --git a/libs/map/dataset/src/model/part-mapbox-source.model.ts b/libs/map/dataset/src/model/part-mapbox-source.model.ts new file mode 100644 index 0000000..1f02bf4 --- /dev/null +++ b/libs/map/dataset/src/model/part-mapbox-source.model.ts @@ -0,0 +1,42 @@ +import { MapSimple } from '@hungpvq/shared-map'; +import { AnySourceData, GeoJSONSourceRaw, RasterSource } from 'mapbox-gl'; +import { IMapboxSourceView } from '../interfaces/dataset.parts'; +import { DatasetLeaf } from './dataset.base'; + +export abstract class DatasetPartMapboxSourceComponent + extends DatasetLeaf + implements IMapboxSourceView +{ + override get type(): string { + return 'source'; + } + abstract getMapboxSource(): AnySourceData; + addToMap(map: MapSimple) { + if (this.id && !map.getSource(this.id)) { + map.addSource(this.id, this.getMapboxSource()); + } + } + removeFromMap(map: MapSimple) { + if (this.id && map.getSource(this.id)) { + map.removeSource(this.id); + } + } +} + +export class GeojsonSource extends DatasetPartMapboxSourceComponent { + override getMapboxSource(): GeoJSONSourceRaw { + return { + type: 'geojson', + data: this.getData() || { + type: 'FeatureCollection', + features: [], + }, + }; + } +} + +export class RasterUrlSource extends DatasetPartMapboxSourceComponent { + override getMapboxSource(): RasterSource { + return this.data; + } +} diff --git a/libs/map/dataset/src/modules/LayerControl/LayerControl.vue b/libs/map/dataset/src/modules/LayerControl/LayerControl.vue new file mode 100644 index 0000000..e4c3d1e --- /dev/null +++ b/libs/map/dataset/src/modules/LayerControl/LayerControl.vue @@ -0,0 +1,225 @@ + + + + + + + diff --git a/libs/map/dataset/src/modules/LayerControl/part/DraggableList/draggable-list-group.vue b/libs/map/dataset/src/modules/LayerControl/part/DraggableList/draggable-list-group.vue new file mode 100644 index 0000000..dd7a80f --- /dev/null +++ b/libs/map/dataset/src/modules/LayerControl/part/DraggableList/draggable-list-group.vue @@ -0,0 +1,205 @@ + + + diff --git a/libs/map/dataset/src/modules/LayerControl/part/DraggableList/draggable-list-item.vue b/libs/map/dataset/src/modules/LayerControl/part/DraggableList/draggable-list-item.vue new file mode 100644 index 0000000..352c29f --- /dev/null +++ b/libs/map/dataset/src/modules/LayerControl/part/DraggableList/draggable-list-item.vue @@ -0,0 +1,74 @@ + + + + + + diff --git a/libs/map/dataset/src/modules/LayerControl/part/DraggableList/draggable-list.vue b/libs/map/dataset/src/modules/LayerControl/part/DraggableList/draggable-list.vue new file mode 100644 index 0000000..3d8f2c8 --- /dev/null +++ b/libs/map/dataset/src/modules/LayerControl/part/DraggableList/draggable-list.vue @@ -0,0 +1,261 @@ + + + diff --git a/libs/map/dataset/src/modules/LayerControl/part/LayerList.vue b/libs/map/dataset/src/modules/LayerControl/part/LayerList.vue new file mode 100644 index 0000000..8ca93e5 --- /dev/null +++ b/libs/map/dataset/src/modules/LayerControl/part/LayerList.vue @@ -0,0 +1,323 @@ + + + + + + diff --git a/libs/map/dataset/src/modules/LayerControl/part/item/layer-item-icon.vue b/libs/map/dataset/src/modules/LayerControl/part/item/layer-item-icon.vue new file mode 100644 index 0000000..76f96d2 --- /dev/null +++ b/libs/map/dataset/src/modules/LayerControl/part/item/layer-item-icon.vue @@ -0,0 +1,28 @@ + + + diff --git a/libs/map/dataset/src/modules/LayerControl/part/item/layer-item-slider.vue b/libs/map/dataset/src/modules/LayerControl/part/item/layer-item-slider.vue new file mode 100644 index 0000000..eee0021 --- /dev/null +++ b/libs/map/dataset/src/modules/LayerControl/part/item/layer-item-slider.vue @@ -0,0 +1,145 @@ + + + + diff --git a/libs/map/dataset/src/modules/LayerControl/part/item/layer-item.vue b/libs/map/dataset/src/modules/LayerControl/part/item/layer-item.vue new file mode 100644 index 0000000..5ba6b6d --- /dev/null +++ b/libs/map/dataset/src/modules/LayerControl/part/item/layer-item.vue @@ -0,0 +1,194 @@ + + + + diff --git a/libs/map/dataset/src/modules/LayerControl/part/item/menu/index.vue b/libs/map/dataset/src/modules/LayerControl/part/item/menu/index.vue new file mode 100644 index 0000000..2cadf85 --- /dev/null +++ b/libs/map/dataset/src/modules/LayerControl/part/item/menu/index.vue @@ -0,0 +1,24 @@ + + + diff --git a/libs/map/dataset/src/modules/LayerControl/part/item/menu/menu-divider.vue b/libs/map/dataset/src/modules/LayerControl/part/item/menu/menu-divider.vue new file mode 100644 index 0000000..77356c1 --- /dev/null +++ b/libs/map/dataset/src/modules/LayerControl/part/item/menu/menu-divider.vue @@ -0,0 +1,17 @@ + + + diff --git a/libs/map/dataset/src/modules/LayerControl/part/item/menu/menu-item.vue b/libs/map/dataset/src/modules/LayerControl/part/item/menu/menu-item.vue new file mode 100644 index 0000000..4d18832 --- /dev/null +++ b/libs/map/dataset/src/modules/LayerControl/part/item/menu/menu-item.vue @@ -0,0 +1,24 @@ + + + diff --git a/libs/map/dataset/src/modules/index.ts b/libs/map/dataset/src/modules/index.ts new file mode 100644 index 0000000..f48efc3 --- /dev/null +++ b/libs/map/dataset/src/modules/index.ts @@ -0,0 +1 @@ +export { default as LayerControl } from './LayerControl/LayerControl.vue'; diff --git a/libs/map/dataset/src/shims-vue.d.ts b/libs/map/dataset/src/shims-vue.d.ts new file mode 100644 index 0000000..8caf263 --- /dev/null +++ b/libs/map/dataset/src/shims-vue.d.ts @@ -0,0 +1,6 @@ +declare module '*.vue' { + import type { DefineComponent } from 'vue'; + const component: DefineComponent; + export default component; +} +declare module '@jamescoyle/vue-icon'; diff --git a/libs/map/dataset/src/store/index.ts b/libs/map/dataset/src/store/index.ts new file mode 100644 index 0000000..6a199c5 --- /dev/null +++ b/libs/map/dataset/src/store/index.ts @@ -0,0 +1,100 @@ +import { MapSimple } from '@hungpvq/shared-map'; +import { + addStore, + addToQueue, + getMap, + getStore, + IListView, +} from '@hungpvq/vue-map-core'; +import { IDataset } from '../interfaces/dataset.base'; +import { + applyToAllLeaves, + findAllComponentsByType, +} from '../model/dataset.visitors'; +import { isComposite, isDatasetMap } from '../utils/check'; + +export const KEY = 'dataset'; +export type MapLayerStore = { + datasets: Record; +}; +export function initMapLayer(mapId: string) { + addStore(mapId, KEY, { datasets: {} }); +} +addToQueue(KEY, initMapLayer); +export async function addDataset(mapId: string, layer: IDataset) { + const store = getStoreDataset(mapId); + if (!store) { + return; + } + const currentLists = getAllComponentsByType( + mapId, + 'list' + ); + const allComponentsOfType = findAllComponentsByType(layer, 'list') as Array< + IListView & IDataset + >; + store.datasets[layer.id] = layer; + allComponentsOfType.forEach((list, i) => { + list.index = i + 1 + currentLists.length; + }); + getMap(mapId, async (map: MapSimple) => { + applyToAllLeaves(layer, [ + (leaf) => { + if (isDatasetMap(leaf)) { + leaf.addToMap(map); + } + }, + ]); + }); +} +export function removeDataset(mapId: string, layer: IDataset) { + const store = getStoreDataset(mapId); + if (!store) { + return; + } + delete store.datasets[layer.id]; + getMap(mapId, async (map: MapSimple) => { + applyToAllLeaves(layer, [ + (leaf) => { + if (isDatasetMap(leaf)) { + leaf.removeFromMap(map); + } + }, + ]); + }); +} +export function removeComponent(mapId: string, component: IDataset) { + getMap(mapId, async (map: MapSimple) => { + const parent = component.getParent() || component; + if (isDatasetMap(component)) { + component.removeFromMap(map); + } + applyToAllLeaves(parent, [ + (leaf) => { + if (isDatasetMap(leaf)) { + leaf.removeFromMap(map); + } + }, + ]); + // Remove component from parent + if (parent && isComposite(parent)) { + parent.remove(component); + } + }); +} +export function getStoreDataset(mapId: string) { + const store = getStore(mapId, KEY); + return store; +} +export function getAllComponentsByType(mapId: string, targetType: string) { + const store = getStoreDataset(mapId); + if (!store) { + return []; + } + const views: T[] = []; + Object.values(store.datasets).forEach((dataset) => { + const allComponentsOfType = findAllComponentsByType(dataset, targetType); + views.push(...(allComponentsOfType as T[])); + }); + return views; +} diff --git a/libs/map/dataset/src/types/vue-icon.d.ts b/libs/map/dataset/src/types/vue-icon.d.ts new file mode 100644 index 0000000..3e5b087 --- /dev/null +++ b/libs/map/dataset/src/types/vue-icon.d.ts @@ -0,0 +1,15 @@ +declare module '@jamescoyle/vue-icon' { + import { DefineComponent } from 'vue'; + + const SvgIcon: DefineComponent<{ + type: string; + path: string; + size?: string | number; + viewBox?: string; + flip?: 'horizontal' | 'vertical' | 'both'; + rotate?: number; + title?: string; + }>; + + export default SvgIcon; +} diff --git a/libs/map/dataset/src/utils/check.ts b/libs/map/dataset/src/utils/check.ts new file mode 100644 index 0000000..6107788 --- /dev/null +++ b/libs/map/dataset/src/utils/check.ts @@ -0,0 +1,29 @@ +import { IDataset } from '../interfaces/dataset.base'; +import { IDatasetMap } from '../interfaces/dataset.map'; +import { IMapboxLayerView } from '../interfaces/dataset.parts'; +import { DatasetComposite } from '../model'; + +// Type guard to check if a dataset implements IDatasetMap +export function isDatasetMap( + dataset: IDataset +): dataset is IDataset & IDatasetMap { + return 'removeFromMap' in dataset && 'addToMap' in dataset; +} + +// Type guard to check if a dataset implements IMapboxLayerView +export function isMapboxLayerView( + dataset: IDataset +): dataset is IDataset & IMapboxLayerView { + return ( + 'toggleShow' in dataset && + 'setOpacity' in dataset && + 'moveLayer' in dataset && + 'getBeforeId' in dataset + ); +} +// Type guard to check if a dataset implements IMapboxLayerView +export function isComposite( + dataset: IDataset +): dataset is IDataset & DatasetComposite { + return dataset.isComposite(); +} diff --git a/libs/map/dataset/tsconfig.json b/libs/map/dataset/tsconfig.json index f2400ab..6197231 100644 --- a/libs/map/dataset/tsconfig.json +++ b/libs/map/dataset/tsconfig.json @@ -1,13 +1,14 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "module": "commonjs", - "forceConsistentCasingInFileNames": true, + "allowJs": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, "strict": true, - "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true + "jsx": "preserve", + "jsxImportSource": "vue", + "moduleResolution": "node", + "resolveJsonModule": true }, "files": [], "include": [], diff --git a/libs/map/dataset/tsconfig.lib.json b/libs/map/dataset/tsconfig.lib.json index be3b140..818697f 100644 --- a/libs/map/dataset/tsconfig.lib.json +++ b/libs/map/dataset/tsconfig.lib.json @@ -2,9 +2,26 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "../../../dist/out-tsc", - "declaration": true, - "types": ["node", "vite/client"] + "types": ["vite/client"] }, - "include": ["src/**/*.ts"], - "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"] + "include": [ + "src/**/*.js", + "src/**/*.jsx", + "src/**/*.ts", + "src/**/*.tsx", + "src/**/*.vue" + ], + "exclude": [ + "src/**/__tests__/*", + "src/**/*.spec.ts", + "src/**/*.test.ts", + "src/**/*.spec.tsx", + "src/**/*.test.tsx", + "src/**/*.spec.js", + "src/**/*.test.js", + "src/**/*.spec.jsx", + "src/**/*.test.jsx", + "src/**/*.spec.vue", + "src/**/*.test.vue" + ] } From fc6b3c5de7abdaae24e281d7e4c97b972a76c0d3 Mon Sep 17 00:00:00 2001 From: "hung.pv" Date: Fri, 11 Apr 2025 17:58:14 +0700 Subject: [PATCH 04/35] feat: add identify --- apps/demo-map/src/layout/aside-control.vue | 2 +- .../src/views/LayerControl/custom-button.vue | 2 +- .../views/LayerControl/custom-change-icon.vue | 4 +- apps/demo-map/src/views/test.vue | 45 ++- .../src/modules/CrsControl/CrsControl.vue | 8 +- .../MouseCoordinatesControl.vue | 6 +- .../dataset/src/interfaces/dataset.parts.ts | 37 +- .../map/dataset/src/model/dataset.visitors.ts | 2 +- libs/map/dataset/src/model/index.ts | 1 + .../dataset/src/model/part-identify.model.ts | 75 ++++ .../src/model/part-mapbox-layer.model.ts | 4 + .../IdentifyControl/IdentifyControl.vue | 365 ++++++++++++++++++ .../modules/IdentifyControl/menu/index.vue | 31 ++ .../IdentifyControl/menu/menu-divider.vue | 18 + .../IdentifyControl/menu/menu-item.vue | 19 + .../src/modules/LayerControl/LayerControl.vue | 2 - .../modules/LayerControl/part/LayerList.vue | 10 +- libs/map/dataset/src/modules/index.ts | 1 + 18 files changed, 607 insertions(+), 25 deletions(-) create mode 100644 libs/map/dataset/src/model/part-identify.model.ts create mode 100644 libs/map/dataset/src/modules/IdentifyControl/IdentifyControl.vue create mode 100644 libs/map/dataset/src/modules/IdentifyControl/menu/index.vue create mode 100644 libs/map/dataset/src/modules/IdentifyControl/menu/menu-divider.vue create mode 100644 libs/map/dataset/src/modules/IdentifyControl/menu/menu-item.vue diff --git a/apps/demo-map/src/layout/aside-control.vue b/apps/demo-map/src/layout/aside-control.vue index 58f06f7..4f9b4cc 100644 --- a/apps/demo-map/src/layout/aside-control.vue +++ b/apps/demo-map/src/layout/aside-control.vue @@ -5,7 +5,7 @@ @click.stop="toggleShow()" :tooltip="trans('map.aside-control.title')" > - + diff --git a/apps/demo-map/src/views/LayerControl/custom-button.vue b/apps/demo-map/src/views/LayerControl/custom-button.vue index 94e9b9c..9539ba5 100644 --- a/apps/demo-map/src/views/LayerControl/custom-button.vue +++ b/apps/demo-map/src/views/LayerControl/custom-button.vue @@ -1,6 +1,6 @@ diff --git a/libs/map/dataset/src/modules/Legend/parts/single-color.vue b/libs/map/dataset/src/modules/Legend/parts/single-color.vue new file mode 100644 index 0000000..fdab1dc --- /dev/null +++ b/libs/map/dataset/src/modules/Legend/parts/single-color.vue @@ -0,0 +1,60 @@ + + + + + diff --git a/libs/map/dataset/src/modules/Legend/parts/single-value.vue b/libs/map/dataset/src/modules/Legend/parts/single-value.vue new file mode 100644 index 0000000..e6455e8 --- /dev/null +++ b/libs/map/dataset/src/modules/Legend/parts/single-value.vue @@ -0,0 +1,42 @@ + + + diff --git a/libs/map/dataset/src/modules/index.ts b/libs/map/dataset/src/modules/index.ts index 841f2b6..c11830b 100644 --- a/libs/map/dataset/src/modules/index.ts +++ b/libs/map/dataset/src/modules/index.ts @@ -3,3 +3,4 @@ export { default as IdentifyControl } from './IdentifyControl/IdentifyControl.vu export { default as LayerControl } from './LayerControl/LayerControl.vue'; export { default as LayerDetail } from './LayerDetail/LayerDetail.vue'; export { default as LayerHighlight } from './LayerHighlight/LayerHighlight.vue'; +export { createLegend, createMultiLegend } from './Legend'; From 40b74e6e120d29a5e8657820841646c5c4d7e340 Mon Sep 17 00:00:00 2001 From: "hung.pv" Date: Mon, 14 Apr 2025 22:42:31 +0700 Subject: [PATCH 12/35] feat: add layer info --- apps/demo-map/src/views/test.vue | 9 + .../modules/LayerControl/LayerInfoControl.vue | 200 +++++++++++ .../modules/LayerControl/part/LayerList.vue | 1 + .../LayerControl/part/LayerListReadonly.vue | 315 ++++++++++++++++++ .../LayerControl/part/item/layer-item.vue | 34 +- libs/map/dataset/src/modules/index.ts | 1 + 6 files changed, 558 insertions(+), 2 deletions(-) create mode 100644 libs/map/dataset/src/modules/LayerControl/LayerInfoControl.vue create mode 100644 libs/map/dataset/src/modules/LayerControl/part/LayerListReadonly.vue diff --git a/apps/demo-map/src/views/test.vue b/apps/demo-map/src/views/test.vue index 642faec..5550754 100644 --- a/apps/demo-map/src/views/test.vue +++ b/apps/demo-map/src/views/test.vue @@ -29,6 +29,7 @@ import { IdentifyMapboxComponent, LayerControl, LayerHighlight, + LayerInfoControl, LayerSimpleMapboxBuild, MultiMapboxLayerComponent, RasterUrlSource, @@ -213,6 +214,9 @@ function onMapLoaded(map: MapSimple) { }, }, ]; + const group = { id: 'test', name: 'test' }; + list1.group = group; + list2.group = group; groupLayer2.add(layer2); groupLayer2.add(list2); groupLayer2.add(metadataForList2); @@ -279,6 +283,11 @@ function onMapLoaded(map: MapSimple) { diff --git a/libs/map/dataset/src/modules/LayerControl/part/LayerList.vue b/libs/map/dataset/src/modules/LayerControl/part/LayerList.vue index 2bbd83f..68234e7 100644 --- a/libs/map/dataset/src/modules/LayerControl/part/LayerList.vue +++ b/libs/map/dataset/src/modules/LayerControl/part/LayerList.vue @@ -32,7 +32,15 @@ const props = defineProps({ disabledDrag: Boolean, disabled: Boolean, }); -const emit = defineEmits(['click:create']); + +defineSlots<{ + title(): any; + item(props: { + item: IListViewUI; + isSelected: boolean; + toggleSelect: (item: IListViewUI) => void; + }): any; +}>(); const path = { icon: mdiLayers, menu: mdiDotsVertical, @@ -132,9 +140,6 @@ function getViewFromStore() { (a, b) => b.index - a.index ) || []; } -function openAddLayer() { - emit('click:create'); -} function addNewGroup() { if (groupRef.value) groupRef.value.addNewGroup(''); } @@ -195,6 +200,7 @@ function onLayerAction({