diff --git a/client/src/components/map/mapUtils.ts b/client/src/components/map/mapUtils.ts index 238d253..6680d55 100644 --- a/client/src/components/map/mapUtils.ts +++ b/client/src/components/map/mapUtils.ts @@ -22,6 +22,7 @@ import VectorImageLayer from "ol/layer/VectorImage"; import VectorSource from "ol/source/Vector"; import Map from "ol/Map"; import { Icon, Style } from "ol/style"; +import axiosInstance from "../../http/axiosInstanceNest"; export function getCitySettings() { return { @@ -94,11 +95,26 @@ export const addFigures = ( if (figuresData.length > 0) { const geoJsonObject = { type: "FeatureCollection", - features: figuresData.map((figure: IFigure) => processFigure( - figure, - settings.scale, - [center[0], center[1]] - )), + features: figuresData.map((figure: IFigure) => { + if (figure.modified) { + console.log("found modified", JSON.parse(figure.modified)) + return { + ...JSON.parse(figure.modified), properties: { + year: figure.year, + figure_type_id: figure.figure_type_id, + type: figure.type, + object_id: figure.object_id, + planning: figure.planning + } + } + } else { + return processFigure( + figure, + settings.scale, + [center[0], center[1]] + ) + } + }), } const features = new GeoJSON().readFeatures(geoJsonObject) @@ -127,7 +143,22 @@ export const addLines = ( if (linesData.length > 0) { const geoJsonObject = { type: "FeatureCollection", - features: linesData.map((line: ILine) => processLine(line, settings.scale, [center[0], center[1]])), + features: linesData.map((line: ILine) => { + if (line.modified) { + return { + ...JSON.parse(line.modified), properties: { + year: line.year, + type: line.type, + geometry_type: 'line', + object_id: line.object_id, + planning: line.planning, + } + } + } else { + return processLine(line, settings.scale, [center[0], center[1]]) + } + + }), } const features = new GeoJSON().readFeatures(geoJsonObject) @@ -144,6 +175,39 @@ export const calculateAngle = (coords: [number, number][]) => { return Math.atan2(dy, dx); // Angle in radians } +export function processLineFeatureToLine( + feature: Feature, + scaling: number, + mapCenter: Coordinate +): Partial { + const geometry = feature.getGeometry() as LineString; + + // Get the line coordinates + const coordinates = geometry.getCoordinates(); + + if (coordinates.length < 2) { + throw new Error('Line must have at least 2 points'); + } + + // Get start and end points + const startPoint = coordinates[0]; + const endPoint = coordinates[coordinates.length - 1]; + + // Convert from map coordinates to database coordinates + const x1 = (startPoint[0] - mapCenter[0]) / scaling; + const y1 = (mapCenter[1] - startPoint[1]) / scaling; + + const x2 = (endPoint[0] - mapCenter[0]) / scaling; + const y2 = (mapCenter[1] - endPoint[1]) / scaling; + + return { + x1: parseFloat(x1.toFixed(6)), + y1: parseFloat(y1.toFixed(6)), + x2: parseFloat(x2.toFixed(6)), + y2: parseFloat(y2.toFixed(6)), + }; +} + export function processLine( line: ILine, scaling: number, @@ -167,6 +231,7 @@ export function processLine( type: "Feature", geometry: new GeoJSON().writeGeometryObject(geometry), properties: { + year: line.year, type: line.type, geometry_type: 'line', object_id: line.object_id, @@ -176,6 +241,158 @@ export function processLine( } } +export function processFeatureToFigure( + feature: Feature, + figureTypeId: number, + scaling: number, + mapCenter: Coordinate, +): Partial { + const geometry = feature.getGeometry() + const properties = feature.getProperties(); + + switch (figureTypeId) { + case 1: // Ellipse + return processEllipseFeature(geometry as Polygon, scaling, mapCenter); + + case 3: // Custom polygon + return processPolygonFeature(geometry as Polygon, scaling, mapCenter); + + case 4: // Rotated rectangle + return processRectangleFeature(geometry as Polygon, scaling, mapCenter, properties.angle); + + default: + throw new Error(`Unsupported figure type: ${figureTypeId}`); + } +} + +function processEllipseFeature(geometry: Polygon, scaling: number, mapCenter: Coordinate): Partial { + // Get the extent of the ellipse + const extent = geometry.getExtent(); + + // Calculate dimensions + const width = (extent[2] - extent[0]) / scaling; + const height = (extent[3] - extent[1]) / scaling; + + // Calculate center in map coordinates + const centerX = (extent[0] + extent[2]) / 2; + const centerY = (extent[1] + extent[3]) / 2; + + // Convert to database coordinates + const left = (centerX - mapCenter[0]) / scaling; + const top = (mapCenter[1] - centerY) / scaling; + + return { + figure_type_id: 1, + width: parseFloat(width.toFixed(6)), + height: parseFloat(height.toFixed(6)), + left: parseFloat(left.toFixed(6)), + top: parseFloat(top.toFixed(6)) + }; +} + +function processPolygonFeature( + geometry: Polygon, + scaling: number, + mapCenter: Coordinate, +): Partial { + // Get polygon coordinates (first ring only) + const coordinates = geometry.getCoordinates()[0]; + + // Calculate the bounding box to get left/top + let minX = Infinity, maxX = -Infinity; + let minY = Infinity, maxY = -Infinity; + + coordinates.forEach(coord => { + minX = Math.min(minX, coord[0]); + maxX = Math.max(maxX, coord[0]); + minY = Math.min(minY, coord[1]); + maxY = Math.max(maxY, coord[1]); + }); + + // Calculate center of the polygon + const centerX = (minX + maxX) / 2; + const centerY = (minY + maxY) / 2; + + // Convert center to database coordinates for left/top + const left = (centerX - mapCenter[0]) / scaling; + const top = (mapCenter[1] - centerY) / scaling; + + // Now convert all points relative to this center + const pointsArray = coordinates.map(coord => { + // Points are relative to (left, top), which is our calculated center + const relativeX = (coord[0] - mapCenter[0]) / scaling - left; + const relativeY = (mapCenter[1] - coord[1]) / scaling - top; + return `${relativeX.toFixed(6)};${relativeY.toFixed(6)}`; + }); + + // Join points with space (skip last point if it's duplicate of first) + const points = pointsArray.slice(0, -1).join(' '); + + return { + figure_type_id: 3, + points, + left: parseFloat(left.toFixed(6)), + top: parseFloat(top.toFixed(6)) + }; +} + +function processRectangleFeature( + geometry: Polygon, + scaling: number, + mapCenter: Coordinate, + angle: number = 0 +): Partial { + // Clone geometry to avoid modifying original + const workingGeometry = geometry.clone(); + + // Get the current center + const currentCenter = getCenter(workingGeometry.getExtent()); + + // If there's rotation, we need to unrotate to get axis-aligned bounds + if (angle !== 0) { + workingGeometry.rotate(angle * Math.PI / 180, currentCenter); + } + + // Get the unrotated extent + const extent = workingGeometry.getExtent(); + + // Calculate dimensions from unrotated geometry + const width = (extent[2] - extent[0]) / scaling; + const height = (extent[3] - extent[1]) / scaling; + + // Calculate center of unrotated geometry + const unrotatedCenter = getCenter(extent); + + // Convert to database coordinates + const left = (unrotatedCenter[0] - mapCenter[0]) / scaling; + const top = (mapCenter[1] - unrotatedCenter[1]) / scaling; + + return { + figure_type_id: 4, + width: parseFloat(width.toFixed(6)), + height: parseFloat(height.toFixed(6)), + left: parseFloat(left.toFixed(6)), + top: parseFloat(top.toFixed(6)), + angle: angle // Preserve the angle + }; +} + +// Helper function to update a feature after movement +export function updateFeatureCoordinates( + feature: Feature, + scaling: number, + mapCenter: Coordinate +): void { + const figureTypeId = feature.get('figure_type_id'); + const updatedFigure = processFeatureToFigure(feature, figureTypeId, scaling, mapCenter); + + // You can now send updatedFigure to your backend + console.log('Updated figure data:', updatedFigure); + + // Example: Update feature properties with database coordinates for reference + feature.set('db_coordinates', updatedFigure); +} + export function processFigure( figure: IFigure, scaling: number, @@ -201,6 +418,8 @@ export function processFigure( type: "Feature", geometry: new GeoJSON().writeGeometryObject(ellipseGeom), properties: { + year: figure.year, + figure_type_id: figure.figure_type_id, type: figure.type, object_id: figure.object_id, planning: figure.planning @@ -229,6 +448,8 @@ export function processFigure( type: "Feature", geometry: new GeoJSON().writeGeometryObject(polygon), properties: { + year: figure.year, + figure_type_id: figure.figure_type_id, type: figure.type, object_id: figure.object_id, planning: figure.planning @@ -264,6 +485,8 @@ export function processFigure( type: "Feature", geometry: new GeoJSON().writeGeometryObject(geometry1), properties: { + year: figure.year, + figure_type_id: figure.figure_type_id, type: figure.type, object_id: figure.object_id, planning: figure.planning, @@ -590,9 +813,54 @@ export const addInteractions = ( } if (currentTool == 'Mover') { - setTranslate(map_id, new Translate({ + const translateMode = new Translate({ features: new Collection(getSelectedFeatures(map_id)) - })) + }) + + translateMode.on('translateend', async (e) => { + const features = e.features.getArray() + + let changesJSON: any = [] + + features.map((f: Feature) => { + if (f.get('geometry_type') === 'line') { + const json = new GeoJSON() + + changesJSON.push({ + object_id: f.get('object_id'), + year: f.get('year'), + type: 'line', + feature: JSON.parse(json.writeFeature(f, { + featureProjection: 'EPSG:3857', + dataProjection: 'EPSG:3857' + })) + }) + } else { + const json = new GeoJSON() + + changesJSON.push({ + object_id: f.get('object_id'), + year: f.get('year'), + type: 'figure', + feature: JSON.parse(json.writeFeature(f, { + featureProjection: 'EPSG:3857', + dataProjection: 'EPSG:3857' + })) + }) + } + }) + + console.log(changesJSON) + + await axiosInstance.post(`/gis/features/update`, { + features: changesJSON + }, + { + baseURL: import.meta.env.VITE_API_NEST_URL, + }) + }) + + setTranslate(map_id, translateMode) const translate = getTranslate(map_id) if (translate) { diff --git a/client/src/interfaces/gis.ts b/client/src/interfaces/gis.ts index fb5856a..ba9f8c8 100644 --- a/client/src/interfaces/gis.ts +++ b/client/src/interfaces/gis.ts @@ -4,7 +4,6 @@ import { SatelliteMapsProvider } from "./map"; import Map from "ol/Map"; import { Coordinate } from "ol/coordinate"; import { Mode } from "../store/map"; - export interface IRegion { id: number name: string @@ -30,7 +29,8 @@ export interface IFigure { label_size: number | null, year: number, type: number, - planning: boolean + planning: boolean, + modified?: string } export interface ILine { @@ -49,7 +49,8 @@ export interface ILine { label_positions: string | null, year: number, type: number, - planning: boolean + planning: boolean, + modified?: string } export interface ICitySettings { diff --git a/client/src/store/map.ts b/client/src/store/map.ts index 8fbf3b0..8d5595b 100644 --- a/client/src/store/map.ts +++ b/client/src/store/map.ts @@ -27,7 +27,6 @@ import { getSelectedRegion, setCurrentObjectId, setSelectedDistrict, setSelected import View from 'ol/View'; import { getPrintOrientation } from './print'; import { getDistrictsData, getRegionsData } from './regions'; -import { fromExtent } from 'ol/geom/Polygon'; export type Mode = 'edit' | 'view' | 'print' @@ -273,7 +272,7 @@ export const initializeMapState = ( condition: noModifierKeys }) - selectionDragBox.on('boxend', (e) => { + selectionDragBox.on('boxend', () => { const extent = selectionDragBox.getGeometry().getExtent() const figuresSource = figuresLayer.getSource() diff --git a/server/ems.db b/server/ems.db index b285df1..88ec319 100644 Binary files a/server/ems.db and b/server/ems.db differ diff --git a/server/src/gis/dto/update-features-batch.ts b/server/src/gis/dto/update-features-batch.ts new file mode 100644 index 0000000..2c2e9c7 --- /dev/null +++ b/server/src/gis/dto/update-features-batch.ts @@ -0,0 +1,8 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsArray } from "class-validator"; + +export class UpdateFeaturesBatchDto { + @ApiProperty() + @IsArray() + features: any[] +} \ No newline at end of file diff --git a/server/src/gis/gis.controller.ts b/server/src/gis/gis.controller.ts index 8d3e357..8882307 100644 --- a/server/src/gis/gis.controller.ts +++ b/server/src/gis/gis.controller.ts @@ -2,6 +2,7 @@ import { Body, Controller, Get, Param, ParseIntPipe, Post, Query } from '@nestjs import { GisService } from './gis.service'; import { ApiBody } from '@nestjs/swagger'; import { BoundsRequestDto } from './dto/bound'; +import { UpdateFeaturesBatchDto } from './dto/update-features-batch'; @Controller('gis') export class GisController { @@ -22,6 +23,11 @@ export class GisController { return await this.gisService.getBoundsByEntityTypeAndId(entity_type, entity_id) } + @Post('/features/update') + async updateFeaturesBatch(@Body() updateFeaturesBatch: UpdateFeaturesBatchDto) { + return await this.gisService.updateFeaturesBatch(updateFeaturesBatch.features) + } + @Post('/bounds/:entity_type') @ApiBody({ type: BoundsRequestDto }) async getBoundsByEntityTypeAndList( diff --git a/server/src/gis/gis.service.ts b/server/src/gis/gis.service.ts index 670e26f..ebabaa7 100644 --- a/server/src/gis/gis.service.ts +++ b/server/src/gis/gis.service.ts @@ -119,26 +119,108 @@ export class GisService { return result } + // async getFigures(offset: number, limit: number, year: number, city_id: number): Promise { + // const result = await this.emsDataSource.query(` + // SELECT o.*, f.[figure_type_id], f.[left], f.[top], f.[width], f.[height], f.[angle], f.[points], f.[label_left], f.[label_top], f.[label_angle], f.[label_size] FROM New_Gis..figures f + // JOIN nGeneral..vObjects o ON o.object_id = f.object_id WHERE o.id_city = ${city_id} AND f.year = ${year} + // ORDER BY f.year + // OFFSET ${Number(offset) || 0} ROWS + // FETCH NEXT ${Number(limit) || 10} ROWS ONLY; + // `) + // return result + // } + + // async getLines(year: number, city_id: number): Promise { + // const result = await this.emsDataSource.query( + // ` + // SELECT o.[object_id], o.[id_city], o.[id_parent], o.[type], o.[planning], o.[activity], o.[kvr], o.[jur], o.[fuel], o.[boiler_id], + // l.[x1], l.[y1], l.[x2], l.[y2], l.[points], l.[label_offset], l.[group_id], l.[show_label], l.[forced_lengths], l.[label_sizes], l.[label_angles], l.[label_positions], l.[year] + // FROM New_Gis..lines l + // JOIN nGeneral..vObjects o ON l.object_id = o.object_id WHERE o.id_city = ${city_id} AND l.year = ${year}; + // ` + // ) + // return result + // } + async getFigures(offset: number, limit: number, year: number, city_id: number): Promise { - const result = await this.emsDataSource.query(` - SELECT o.*, f.[figure_type_id], f.[left], f.[top], f.[width], f.[height], f.[angle], f.[points], f.[label_left], f.[label_top], f.[label_angle], f.[label_size] FROM New_Gis..figures f - JOIN nGeneral..vObjects o ON o.object_id = f.object_id WHERE o.id_city = ${city_id} AND f.year = ${year} - ORDER BY f.year - OFFSET ${Number(offset) || 0} ROWS - FETCH NEXT ${Number(limit) || 10} ROWS ONLY; - `) - return result + // Get original figures from EMS + const originalFigures = await this.emsDataSource.query(` + SELECT o.[object_id], o.[id_city], o.[id_parent], o.[type], o.[planning], o.[activity], o.[kvr], o.[jur], o.[fuel], o.[boiler_id], f.[figure_type_id], f.[left], f.[top], f.[width], f.[height], f.[angle], f.[points], f.[label_left], f.[label_top], f.[label_angle], f.[label_size], f.[year] FROM New_Gis..figures f + JOIN nGeneral..vObjects o ON o.object_id = f.object_id WHERE o.id_city = ${city_id} AND f.year = ${year} + ORDER BY f.year + OFFSET ${Number(offset) || 0} ROWS + FETCH NEXT ${Number(limit) || 10} ROWS ONLY; + `) + + // Get modified figures from SQLite for the same year + const modifiedFigures = await this.dataSource.query( + `SELECT * FROM figures WHERE year = ?`, + [year] + ) + + // Create a lookup map using object_id + year as key + const modifiedMap = new Map() + modifiedFigures.forEach(fig => { + const key = `${fig.object_id}_${fig.year}` + modifiedMap.set(key, fig) + }) + + // Replace original values with modified ones + const mergedResult = originalFigures.map(original => { + const key = `${original.object_id}_${year}` + const modified = modifiedMap.get(key) + + if (modified) { + return { + ...original, + modified: modified.feature + } + } + + return original + }) + + return mergedResult } async getLines(year: number, city_id: number): Promise { - const result = await this.emsDataSource.query( - ` - SELECT * FROM New_Gis..lines l - JOIN nGeneral..vObjects o ON l.object_id = o.object_id WHERE o.id_city = ${city_id} AND l.year = ${year}; - ` + // Get original lines from EMS + const originalLines = await this.emsDataSource.query(` + SELECT o.[object_id], o.[id_city], o.[id_parent], o.[type], o.[planning], o.[activity], o.[kvr], o.[jur], o.[fuel], o.[boiler_id], + l.[x1], l.[y1], l.[x2], l.[y2], l.[points], l.[label_offset], l.[group_id], l.[show_label], l.[forced_lengths], l.[label_sizes], l.[label_angles], l.[label_positions], l.[year] + FROM New_Gis..lines l + JOIN nGeneral..vObjects o ON l.object_id = o.object_id WHERE o.id_city = ${city_id} AND l.year = ${year}; + `) + + // Get modified lines from SQLite for the same year + const modifiedLines = await this.dataSource.query( + `SELECT * FROM lines WHERE year = ?`, + [year] ) - return result + // Create lookup map with object_id + year + const modifiedMap = new Map() + modifiedLines.forEach(line => { + const key = `${line.object_id}_${line.year}` + modifiedMap.set(key, line) + }) + + // Replace original with modified + const mergedResult = originalLines.map(original => { + const key = `${original.object_id}_${year}` + const modified = modifiedMap.get(key) + + if (modified) { + return { + ...original, + modified: modified.feature + } + } + + return original + }) + + return mergedResult } async getRegionBorders(): Promise { @@ -150,4 +232,63 @@ export class GisService { return result } + + async updateFeaturesBatch(features: any[]) { + let figures: any[] = [] + let lines: any[] = [] + + features.map(feature => { + if (feature.type === 'figure') { + figures.push(feature) + } else if (feature.type === 'line') { + lines.push(feature) + } + }) + + console.log('Figures to update:', figures.length) + console.log('Lines to update:', lines.length) + + // Update figures + if (figures.length > 0) { + const figurePlaceholders = figures.map(() => + `(?, ?, ?)` + ).join(', ') + + const figureParams = figures.flatMap(fig => [ + fig.object_id, + fig.year, // Default year if not provided + JSON.stringify(fig.feature) + ]) + + await this.dataSource.query(` + INSERT OR REPLACE INTO figures + (object_id, year, feature) + VALUES ${figurePlaceholders} + `, figureParams) + } + + // Update lines + if (lines.length > 0) { + const linePlaceholders = lines.map(() => + `(?, ?, ?)` + ).join(', ') + + const lineParams = lines.flatMap(line => [ + line.object_id, + line.year, + JSON.stringify(line.feature) + ]) + + await this.dataSource.query(` + INSERT OR REPLACE INTO lines + (object_id, year, feature) + VALUES ${linePlaceholders} + `, lineParams) + } + + return { + success: true, + message: `Updated ${figures.length} figures and ${lines.length} lines` + } + } } diff --git a/server/src/main.ts b/server/src/main.ts index 63aa087..68462f3 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -2,12 +2,14 @@ import { NestFactory } from '@nestjs/core'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { AppModule } from './app.module'; import { ValidationPipe } from '@nestjs/common'; +import { json } from 'express'; async function bootstrap() { const app = await NestFactory.create(AppModule, { cors: true }); app.enableCors() + app.use(json({ limit: '50mb' })) app.useGlobalPipes(new ValidationPipe({ transform: true })) const config = new DocumentBuilder() .setTitle('Fuel API')