nestjs rewrite

This commit is contained in:
popovspiridon99
2025-08-01 11:33:40 +09:00
parent 145827ab6d
commit 37bfa912a0
58 changed files with 17130 additions and 0 deletions

56
server/.gitignore vendored Normal file
View File

@ -0,0 +1,56 @@
# compiled output
/dist
/node_modules
/build
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# temp directory
.temp
.tmp
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

4
server/.prettierrc Normal file
View File

@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

98
server/README.md Normal file
View File

@ -0,0 +1,98 @@
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Project setup
```bash
$ npm install
```
## Compile and run the project
```bash
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
```
## Run tests
```bash
# unit tests
$ npm run test
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:cov
```
## Deployment
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
```bash
$ npm install -g @nestjs/mau
$ mau deploy
```
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
## Resources
Check out a few resources that may come in handy when working with NestJS:
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).

BIN
server/ems.db Normal file

Binary file not shown.

34
server/eslint.config.mjs Normal file
View File

@ -0,0 +1,34 @@
// @ts-check
import eslint from '@eslint/js';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import globals from 'globals';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{
ignores: ['eslint.config.mjs'],
},
eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
eslintPluginPrettierRecommended,
{
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
sourceType: 'commonjs',
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'warn',
'@typescript-eslint/no-unsafe-argument': 'warn'
},
},
);

8
server/nest-cli.json Normal file
View File

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

15089
server/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

86
server/package.json Normal file
View File

@ -0,0 +1,86 @@
{
"name": "server",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/axios": "^4.0.0",
"@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/swagger": "^11.2.0",
"@nestjs/typeorm": "^11.0.0",
"axios": "^1.9.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"mssql": "^11.0.1",
"pg": "^8.16.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"sharp": "^0.34.2",
"sqlite3": "^5.1.7",
"typeorm": "^0.3.24"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.18.0",
"@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@swc/cli": "^0.6.0",
"@swc/core": "^1.10.7",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.14",
"@types/multer": "^1.4.13",
"@types/node": "^22.10.7",
"@types/supertest": "^6.0.2",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
"globals": "^16.0.0",
"jest": "^29.7.0",
"prettier": "^3.4.2",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.2.5",
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

View File

@ -0,0 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});

View File

@ -0,0 +1,12 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}

86
server/src/app.module.ts Normal file
View File

@ -0,0 +1,86 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { FuelModule } from './fuel/fuel.module';
import { GeneralModule } from './general/general.module';
import { EmsModule } from './ems/ems.module';
import { TilesModule } from './tiles/tiles.module';
import { GisModule } from './gis/gis.module';
@Module({
imports: [
ConfigModule.forRoot(),
TypeOrmModule.forRoot({
type: 'sqlite',
database: 'ems.db',
synchronize: true,
name: 'sqliteConnection'
}),
TypeOrmModule.forRoot({
type: 'mssql',
host: process.env.FUEL_DB_HOST,
port: 1433,
username: process.env.FUEL_DB_USER,
password: process.env.FUEL_DB_PASSWORD,
database: 'isFuels',
options: {
encrypt: false,
trustServerCertificate: true
},
synchronize: false,
autoLoadEntities: true,
name: 'fuelConnection'
}),
TypeOrmModule.forRoot({
type: 'mssql',
host: process.env.EMS_DB_HOST,
port: Number(process.env.EMS_DB_PORT) || 1433,
username: process.env.EMS_DB_USER,
password: process.env.EMS_DB_PASSWORD,
database: process.env.EMS_DB_NAME,
options: {
encrypt: false,
trustServerCertificate: true
},
synchronize: false,
autoLoadEntities: true,
name: 'emsConnection'
}),
TypeOrmModule.forRoot({
type: 'mssql',
host: process.env.GENERAL_DB_HOST,
port: Number(process.env.GENERAL_DB_PORT) || 1433,
username: process.env.GENERAL_DB_USER,
password: process.env.GENERAL_DB_PASSWORD,
database: process.env.GENERAL_DB_NAME,
options: {
encrypt: false,
trustServerCertificate: true
},
synchronize: false,
autoLoadEntities: true,
name: 'generalConnection'
}),
// TypeOrmModule.forRoot({
// type: 'postgres',
// host: process.env.PG_HOST,
// port: Number(process.env.PG_PORT) || 5432,
// username: process.env.PG_USER,
// password: process.env.PG_PASSWORD,
// database: process.env.PG_DB,
// synchronize: false,
// autoLoadEntities: true,
// name: 'pgConnection'
// }),
FuelModule,
GeneralModule,
EmsModule,
TilesModule,
GisModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule { }

View File

@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

View File

@ -0,0 +1,12 @@
import { Extent } from "../interfaces/map"
const epsg3857extent = [
-20037508.342789244,
-20037508.342789244,
20037508.342789244,
20037508.342789244
] as Extent
export {
epsg3857extent
}

View File

@ -0,0 +1,22 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"
import { IsNumberString, IsOptional } from "class-validator"
export class GetFiguresDTO {
@ApiProperty()
@IsNumberString()
city_id: number
@ApiPropertyOptional()
@IsNumberString()
@IsOptional()
offset?: number
@ApiPropertyOptional()
@IsNumberString()
@IsOptional()
limit?: number
@ApiProperty()
@IsNumberString()
year: number
}

View File

@ -0,0 +1,19 @@
import { ApiPropertyOptional } from "@nestjs/swagger"
import { IsNumberString, IsOptional } from "class-validator"
export class GetImagesDTO {
@ApiPropertyOptional()
@IsNumberString()
@IsOptional()
city_id?: number
@ApiPropertyOptional()
@IsNumberString()
@IsOptional()
offset?: number
@ApiPropertyOptional()
@IsNumberString()
@IsOptional()
limit?: number
}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { EmsController } from './ems.controller';
describe('EmsController', () => {
let controller: EmsController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [EmsController],
}).compile();
controller = module.get<EmsController>(EmsController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,26 @@
import { Controller, Get, Query } from '@nestjs/common';
import { EmsService } from './ems.service';
import { GetImagesDTO } from './dto/get-images';
import { GetFiguresDTO } from './dto/get-figures';
@Controller('ems')
export class EmsController {
constructor(private readonly emsService: EmsService) { }
@Get('/regions')//✅
async getRegions() {
return this.emsService.getTypeRoles()
}
@Get('/images')//✅
async getImages(@Query() getImagesDTO: GetImagesDTO) {
const { city_id, limit, offset } = getImagesDTO
return this.emsService.getImages(city_id, offset, limit)
}
@Get('/figures')
async getFigures(@Query() getFiguresDTO: GetFiguresDTO) {
const { offset, limit, year, city_id } = getFiguresDTO
return this.emsService.getFigures(year, city_id, offset, limit)
}
}

View File

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { EmsController } from './ems.controller';
import { EmsService } from './ems.service';
@Module({
controllers: [EmsController],
providers: [EmsService]
})
export class EmsModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { EmsService } from './ems.service';
describe('EmsService', () => {
let service: EmsService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [EmsService],
}).compile();
service = module.get<EmsService>(EmsService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,40 @@
import { Injectable } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
@Injectable()
export class EmsService {
constructor(
@InjectDataSource('emsConnection')
private dataSource: DataSource
) { }
async getTypeRoles(): Promise<any[]> {
const result = await this.dataSource.query(`
SELECT * FROM "TypeRoles";
`)
return result
}
async getImages(city_id?: number, offset?: number, limit?: number): Promise<any[]> {
const result = await this.dataSource.query(`
SELECT * FROM "images"
${city_id ? `WHERE city_id = ${city_id}` : ''}
ORDER BY city_id
OFFSET ${offset || 0} ROWS
FETCH NEXT ${limit || 10} ROWS ONLY;
`)
return result
}
async getFigures(year: number, city_id: number, offset?: number, limit?: number): Promise<any[]> {
const result = await this.dataSource.query(`
SELECT * FROM figures f
JOIN vObjects o ON f.object_id = o.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
}
}

View File

@ -0,0 +1,22 @@
import { ApiProperty } from "@nestjs/swagger"
import { Type } from "class-transformer"
import { IsDate, IsNumber, IsUUID } from "class-validator"
export class CreateExpenseDto {
@ApiProperty({ format: 'uuid' })
@IsUUID()
id_boiler: string
@ApiProperty()
@IsNumber()
id_fuel: number
@ApiProperty({ type: String, format: 'date-time' })
@Type(() => Date)
@IsDate()
date: Date
@ApiProperty()
@IsNumber()
value: number
}

View File

@ -0,0 +1,24 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsNumber, IsUUID } from "class-validator";
export class CreateLimitDto {
@ApiProperty({ format: 'uuid' })
@IsUUID()
id_boiler: string
@ApiProperty()
@IsNumber()
id_fuel: number
@ApiProperty()
@IsNumber()
month: Date
@ApiProperty()
@IsNumber()
year: number
@ApiProperty()
@IsNumber()
value: number
}

View File

@ -0,0 +1,26 @@
import { ApiProperty } from "@nestjs/swagger"
import { Type } from "class-transformer"
import { IsDate, IsNumber, IsUUID } from "class-validator"
export class CreateTransferDto {
@ApiProperty({ format: 'uuid' })
@IsUUID()
id_out: string
@ApiProperty({ format: 'uuid' })
@IsUUID()
id_in: string
@ApiProperty()
@IsNumber()
id_fuel: number
@ApiProperty({ type: String, format: 'date-time' })
@Type(() => Date)
@IsDate()
date: Date
@ApiProperty()
@IsNumber()
value: number
}

View File

@ -0,0 +1,22 @@
import { ApiProperty } from "@nestjs/swagger"
import { Type } from "class-transformer"
import { IsDate, IsNumber, IsUUID } from "class-validator"
export class FuelExpenseDto {
@ApiProperty({ format: 'uuid' })
@IsUUID()
id_boiler: string
@ApiProperty()
@IsNumber()
id_fuel: number
@ApiProperty({ type: String, format: 'date-time' })
@Type(() => Date)
@IsDate()
date: Date
@ApiProperty()
@IsNumber()
value: number
}

View File

@ -0,0 +1,24 @@
import { ApiProperty } from "@nestjs/swagger"
import { IsNumber, IsUUID } from "class-validator"
export class FuelLimitDto {
@ApiProperty({ format: 'uuid' })
@IsUUID()
id_boiler: string
@ApiProperty()
@IsNumber()
id_fuel: number
@ApiProperty()
@IsNumber()
month: Date
@ApiProperty()
@IsNumber()
year: number
@ApiProperty()
@IsNumber()
value: number
}

View File

@ -0,0 +1,26 @@
import { ApiProperty } from "@nestjs/swagger"
import { Type } from "class-transformer"
import { IsDate, IsNumber, IsUUID } from "class-validator"
export class FuelTransferDto {
@ApiProperty({ format: 'uuid' })
@IsUUID()
id_out: string
@ApiProperty({ format: 'uuid' })
@IsUUID()
id_in: string
@ApiProperty()
@IsNumber()
id_fuel: number
@ApiProperty({ type: String, format: 'date-time' })
@Type(() => Date)
@IsDate()
date: Date
@ApiProperty()
@IsNumber()
value: number
}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { FuelController } from './fuel.controller';
describe('FuelController', () => {
let controller: FuelController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [FuelController],
}).compile();
controller = module.get<FuelController>(FuelController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,73 @@
import { Body, Controller, Get, Post } from '@nestjs/common'
import { FuelService } from './fuel.service'
import { CreateExpenseDto } from './dto/create-expense'
import { CreateLimitDto } from './dto/create-limit'
import { CreateTransferDto } from './dto/create-transfer'
import { FuelLimitDto } from './dto/limit'
import { ApiOkResponse } from '@nestjs/swagger'
import { FuelExpenseDto } from './dto/expense'
import { FuelTransferDto } from './dto/transfer'
@Controller('fuel')
export class FuelController {
constructor(private readonly fuelService: FuelService) { }
@Get('/columns')
async getColumnDataTypes() {
return this.fuelService.getColumnDataTypes('BoilersFuelsExpenses')
}
// Fuel limits
@Get('/limits')
@ApiOkResponse({
description: 'List of fuel expenses',
type: FuelLimitDto,
isArray: true,
})
async getBoilersFuelsLimits(): Promise<FuelLimitDto[]> {
return this.fuelService.getBoilersFuelsLimits()
}
@Post('/limits')
async addBoilersFuelsLimit(@Body() createLimitDto: CreateLimitDto) {
return this.fuelService.addBoilersFuelsLimit(createLimitDto)
}
// Fuel expenses
@Get('/expenses')
@ApiOkResponse({
description: 'List of fuel expenses',
type: FuelExpenseDto,
isArray: true,
})
async getBoilersFuelsExpenses(): Promise<FuelExpenseDto[]> {
return this.fuelService.getBoilersFuelsExpenses()
}
@Post('/expenses')
async addBoilersFuelsExpense(@Body() createExpenseDto: CreateExpenseDto) {
return this.fuelService.addBoilersFuelsExpense(createExpenseDto)
}
// Fuel transfer
@Get('/transfer')
@ApiOkResponse({
description: 'List of fuel expenses',
type: FuelTransferDto,
isArray: true,
})
async getFuelsTransfer(): Promise<FuelTransferDto[]> {
return this.fuelService.getFuelsTransfer()
}
@Post('/transfer')
async addFuelTransfer(@Body() createTransferDto: CreateTransferDto) {
return this.fuelService.addFuelsTransfer(createTransferDto)
}
}

View File

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { FuelController } from './fuel.controller';
import { FuelService } from './fuel.service';
@Module({
controllers: [FuelController],
providers: [FuelService]
})
export class FuelModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { FuelService } from './fuel.service';
describe('FuelService', () => {
let service: FuelService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [FuelService],
}).compile();
service = module.get<FuelService>(FuelService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,68 @@
import { Injectable } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import { CreateExpenseDto } from './dto/create-expense';
import { CreateLimitDto } from './dto/create-limit';
import { CreateTransferDto } from './dto/create-transfer';
@Injectable()
export class FuelService {
constructor(
@InjectDataSource('fuelConnection') private dataSource: DataSource
) { }
async getColumnDataTypes(table_name: string): Promise<any[]> {
const result = await this.dataSource.query(`
SELECT
COLUMN_NAME,
DATA_TYPE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = '${table_name}'
`)
return result
}
async getBoilersFuelsExpenses(): Promise<any[]> {
const result = await this.dataSource.query(`
SELECT *
FROM "BoilersFuelsExpenses";
`)
return result
}
async getBoilersFuelsLimits(): Promise<any[]> {
const result = await this.dataSource.query(`
SELECT *
FROM "BoilersFuelsLimits";
`)
return result
}
async addBoilersFuelsExpense(createExpenseDto: CreateExpenseDto): Promise<any[]> {
const result = await this.dataSource.query(`
INSERT INTO dbo.BoilersFuelsExpenses (id_boiler, id_fuel, date, value) VALUES ($1, $2, $3, $4)
`, [createExpenseDto.id_boiler, createExpenseDto.id_fuel, createExpenseDto.date, createExpenseDto.value])
return result
}
async addBoilersFuelsLimit(createLimitDto: CreateLimitDto): Promise<any[]> {
const result = await this.dataSource.query(`
INSERT INTO dbo.BoilersFuelsLimits (id_boiler, id_fuel, value, month, year) VALUES ($1, $2, $3, $4, $5)
`, [createLimitDto.id_boiler, createLimitDto.id_fuel, createLimitDto.value, createLimitDto.month, createLimitDto.year])
return result
}
async getFuelsTransfer(): Promise<any[]> {
const result = await this.dataSource.query(`
SELECT * FROM "FuelsTransfer";
`)
return result
}
async addFuelsTransfer(createTransferDto: CreateTransferDto): Promise<any[]> {
const result = await this.dataSource.query(`
INSERT INTO dbo.FuelsTransfer (id_out, id_in, id_fuel, date, value) VALUES ($1, $2, $3, $4, $5)
`, [createTransferDto.id_out, createTransferDto.id_in, createTransferDto.id_fuel, createTransferDto.date, createTransferDto.value])
return result
}
}

View File

@ -0,0 +1,24 @@
import { ApiPropertyOptional } from "@nestjs/swagger"
import { IsNumberString, IsOptional, IsString } from "class-validator"
export class GetCitiesDTO {
@ApiPropertyOptional()
@IsNumberString()
@IsOptional()
limit?: number
@ApiPropertyOptional()
@IsNumberString()
@IsOptional()
offset?: number
@ApiPropertyOptional()
@IsString()
@IsOptional()
search?: string
@ApiPropertyOptional()
@IsNumberString()
@IsOptional()
id?: number
}

View File

@ -0,0 +1,19 @@
import { ApiPropertyOptional } from "@nestjs/swagger"
import { IsNumber, IsNumberString, IsOptional, IsString } from "class-validator"
export class GetObjectsDTO {
@ApiPropertyOptional()
@IsNumberString()
@IsOptional()
offset?: number
@ApiPropertyOptional()
@IsNumberString()
@IsOptional()
limit?: number
@ApiPropertyOptional()
@IsNumberString()
@IsOptional()
city_id?: number
}

View File

@ -0,0 +1,17 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"
import { IsNumber, IsOptional, IsString } from "class-validator"
export class GetTCBParamsDTO {
@ApiProperty()
@IsString()
vtable: string
@ApiPropertyOptional()
@IsNumber()
@IsOptional()
id?: number
@ApiProperty()
@IsNumber()
id_city: number
}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { GeneralController } from './general.controller';
describe('GeneralController', () => {
let controller: GeneralController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [GeneralController],
}).compile();
controller = module.get<GeneralController>(GeneralController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,67 @@
import { Controller, Get, Param, ParseUUIDPipe, Query } from '@nestjs/common';
import { GeneralService } from './general.service';
import { GetCitiesDTO } from './dto/get-cities';
import { GetObjectsDTO } from './dto/get-objects';
@Controller('/general')
export class GeneralController {
constructor(private readonly generalService: GeneralService) { }
@Get('/regions')//✅
async getRegions() {
return this.generalService.getRegions()
}
@Get('/districts')//✅
async getDistricts(@Query('region_id') region_id: number) {
return this.generalService.getDistricts(region_id)
}
@Get('/cities')//✅
async getCities(@Query() getCitiesDTO: GetCitiesDTO) {
const { offset, limit, search, id } = getCitiesDTO
return this.generalService.getCities(offset, limit, search, id)
}
@Get('/types')//✅
async getTypes() {
return this.generalService.getTypes()
}
@Get('/objects/all')//✅
async getObjects(@Query() getObjectsDTO: GetObjectsDTO) {
const { offset, limit, city_id } = getObjectsDTO
return this.generalService.getObjects(offset, limit, city_id)
}
@Get('/objects/list')// ✅
async getObjectsList(@Query('city_id') city_id: number, @Query('year') year: number, @Query('planning') planning: number, @Query('type') type?: number) {
return this.generalService.getObjectsList(city_id, year, planning, type)
}
@Get('/objects/by-id/:id')//✅
async getObjectById(@Param('id', new ParseUUIDPipe()) id: string) {
return this.generalService.getObjectById(id)
}
@Get('/values')// ✅
async getValues(@Query('object_id') object_id: string) {
return this.generalService.getValuesByObjectId(object_id)
}
@Get('/params')//✅
async getParams(@Query('param_id') param_id: number) {
return this.generalService.getParamsById(param_id)
}
@Get('/params/tcb')// ✅
async getTcbParams(@Query('vtable') vtable: string, @Query('id_city') id_city: number, @Query('id') id: number) {
return this.generalService.getTCBParams(vtable, id_city, id)
}
@Get('/search/objects')// ✅
async getSearchObjects(@Query('q') q: string, @Query('id_city') id_city: number, @Query('year') year: number) {
return this.generalService.getSearchObjects(q, id_city, year)
}
}

View File

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { GeneralController } from './general.controller';
import { GeneralService } from './general.service';
@Module({
controllers: [GeneralController],
providers: [GeneralService]
})
export class GeneralModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { GeneralService } from './general.service';
describe('GeneralService', () => {
let service: GeneralService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [GeneralService],
}).compile();
service = module.get<GeneralService>(GeneralService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,250 @@
import { Injectable } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
@Injectable()
export class GeneralService {
constructor(
@InjectDataSource('emsConnection')
private dataSource: DataSource
) { }
async getRegions(): Promise<any[]> {
const generalDatabase = 'nGeneral'
const result = await this.dataSource.query(`
SELECT * FROM ${generalDatabase}..vRegions;
`)
return result
}
async getDistricts(region_id: number): Promise<any[]> {
const generalDatabase = 'nGeneral'
const result = await this.dataSource.query(`
SELECT c.*, d.name AS district_name
FROM ${generalDatabase}..vCities c
JOIN ${generalDatabase}..vDistricts d ON d.id_region = c.id_region AND d.id = c.id_district
WHERE c.id_region = ${region_id};
`)
return result
}
async getCities(offset?: number, limit?: number, search?: string, id?: number): Promise<any[]> {
const generalDatabase = 'nGeneral'
const result = await this.dataSource.query(`
SELECT * FROM ${generalDatabase}..Cities
${id ? `WHERE id = ${id}` : ''}
${search ? `WHERE name LIKE '%${search || ''}%'` : ''}
ORDER BY id
OFFSET ${Number(offset) || 0} ROWS
FETCH NEXT ${Number(limit) || 10} ROWS ONLY;
`)
return result
}
async getTypes(): Promise<any[]> {
const generalDatabase = 'nGeneral'
const result = await this.dataSource.query(`
SELECT * FROM ${generalDatabase}..tTypes
ORDER BY id
`)
return result
}
async getObjects(offset?: number, limit?: number, city_id?: number): Promise<any[]> {
const generalDatabase = 'nGeneral'
const result = await this.dataSource.query(`
SELECT * FROM ${generalDatabase}..vObjects
${city_id ? `WHERE id_city = ${Number(city_id)}` : ''}
ORDER BY id_object
OFFSET ${Number(offset) || 0} ROWS
FETCH NEXT ${Number(limit) || 10} ROWS ONLY;
`)
return result
}
// TODO: GisDB
async getObjectsList(city_id: number, year: number, planning: number, type?: number): Promise<any[]> {
const generalDatabase = 'nGeneral'
const gisDatabase = 'New_Gis'
const result = await this.dataSource.query(type ? `
WITH cte_split(type_id, split_value, caption_params) AS
(
-- anchor member
SELECT DISTINCT
type_id,
CAST(LEFT(caption_params, CHARINDEX(',', caption_params + ',') - 1) AS VARCHAR(255)), -- Explicitly casting to VARCHAR
STUFF(caption_params, 1, CHARINDEX(',', caption_params + ','), '')
FROM ${gisDatabase}..caption_params
WHERE city_id = -1 AND user_id = -1
UNION ALL
-- recursive member
SELECT
type_id,
CAST(LEFT(caption_params, CHARINDEX(',', caption_params + ',') - 1) AS VARCHAR(255)), -- Explicitly casting to VARCHAR
STUFF(caption_params, 1, CHARINDEX(',', caption_params + ','), '')
FROM cte_split
WHERE caption_params > ''
)
SELECT
o.object_id,
o.type,
o.id_city,
o.year,
o.planning,
string_agg(cast(v.value as varchar), ',') as caption
FROM ${generalDatabase}..vObjects o
JOIN cte_split c ON o.type = c.type_id
JOIN ${generalDatabase}..tParameters p ON p.id = split_value
LEFT JOIN ${generalDatabase}..tValues v
ON
v.id_param = split_value
AND v.id_object = o.object_id
AND (v.date_po IS NULL)
AND (v.date_s < DATEFROMPARTS(${Number(year) + 1},01,01))
WHERE
o.id_city = ${city_id}
AND o.year = ${year}
AND o.type = ${type}
AND
(
CASE
WHEN TRY_CAST(o.planning AS BIT) IS NOT NULL THEN TRY_CAST(o.planning AS BIT)
WHEN o.planning = 'TRUE' THEN 1
WHEN o.planning = 'FALSE' THEN 0
ELSE NULL
END
) = ${planning}
GROUP BY object_id, type, id_city, year, planning;
`:
`
SELECT
${generalDatabase}..tTypes.id AS id,
${generalDatabase}..tTypes.name AS name,
COUNT(vo.type) AS count,
tr.r,
tr.g,
tr.b
FROM
${generalDatabase}..vObjects vo
JOIN
${generalDatabase}..tTypes ON vo.type = ${generalDatabase}..tTypes.id
LEFT JOIN ${gisDatabase}..TypeRoles tr ON tr.id = ${generalDatabase}..tTypes.id
WHERE
vo.id_city = ${city_id} AND vo.year = ${year}
AND
(
CASE
WHEN TRY_CAST(vo.planning AS BIT) IS NOT NULL THEN TRY_CAST(vo.planning AS BIT)
WHEN vo.planning = 'TRUE' THEN 1
WHEN vo.planning = 'FALSE' THEN 0
ELSE NULL
END
) = ${planning}
GROUP BY
${generalDatabase}..tTypes.id,
${generalDatabase}..tTypes.name,
tr.r,
tr.g,
tr.b;
`
)
return result
}
async getObjectById(id: string): Promise<any[]> {
const generalDatabase = 'nGeneral'
const gisDatabase = 'New_Gis'
const result = await this.dataSource.query(`
SELECT * FROM ${generalDatabase}..vObjects
WHERE id_object = '${id}'
`)
return result
}
async getValuesByObjectId(object_id: string): Promise<any[]> {
const generalDatabase = 'nGeneral'
const result = await this.dataSource.query(`
SELECT id_object, id_param, CAST(v.value AS varchar(max)) AS value,
date_s,
date_po,
id_user
FROM ${generalDatabase}..tValues v
JOIN ${generalDatabase}..tParameters p ON v.id_param = p.id
WHERE id_object = '${object_id}'
`)
return result
}
async getParamsById(param_id: number): Promise<any[]> {
const generalDatabase = 'nGeneral'
const result = await this.dataSource.query(`
SELECT * FROM ${generalDatabase}..TParameters
WHERE id = '${param_id}'
`)
return result
}
tcbParamQuery = (vtable: string, id_city: number) => {
const generalDatabase = 'nGeneral'
switch (vtable) {
case 'vStreets':
return `SELECT * FROM ${generalDatabase}..${vtable} WHERE id_city = ${id_city};`
case 'vBoilers':
return `SELECT * FROM ${generalDatabase}..${vtable} WHERE id_city = ${id_city};`
default:
return `SELECT * FROM ${generalDatabase}..${vtable};`
}
}
async getTCBParams(vtable: string, id_city: number, id?: number): Promise<any[]> {
const generalDatabase = 'nGeneral'
const query = id ? `
SELECT * FROM ${generalDatabase}..${vtable} WHERE id = '${id}'
` : this.tcbParamQuery(vtable, id_city)
const result = await this.dataSource.query(query)
return result
}
async getSearchObjects(q: string, id_city: number, year: number) {
const generalDatabase = 'nGeneral'
const gisDatabase = 'New_Gis'
const result = await this.dataSource.query(`
WITH RankedValues AS (
SELECT
id_object,
date_s,
CAST(value AS varchar(max)) AS value,
ROW_NUMBER() OVER (PARTITION BY id_object ORDER BY date_s DESC) AS rn,
o.id_city AS id_city,
o.year AS year
FROM ${generalDatabase}..tValues
JOIN ${generalDatabase}..tObjects o ON o.id = id_object
WHERE CAST(value AS varchar(max)) LIKE '%${q}%'
)
SELECT
id_object,
date_s,
value,
id_city,
year
FROM RankedValues
WHERE rn = 1 AND id_city = ${id_city} AND year = ${year};
`)
return result
}
}

View File

@ -0,0 +1,7 @@
import { Entity, PrimaryGeneratedColumn } from "typeorm"
// @Entity()
// export class Bound {
// @PrimaryGeneratedColumn()
// id:
// }

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { GisController } from './gis.controller';
describe('GisController', () => {
let controller: GisController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [GisController],
}).compile();
controller = module.get<GisController>(GisController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,42 @@
import { Controller, Get, Param, ParseIntPipe, Query } from '@nestjs/common';
import { GisService } from './gis.service';
@Controller('gis')
export class GisController {
constructor(private readonly gisService: GisService) { }
@Get('/type-roles')
async getTypeRoles() {
return await this.gisService.getTypeRoles()
}
@Get('/bounds/:entity_type')
async getBoundsByEntityType(@Param('entity_type') entity_type: 'region' | 'district' | 'city') {
return await this.gisService.getBoundsByEntityType(entity_type)
}
@Get('/bounds/:entity_type/:entity_id')
async getBoundsByEntityTypeAndId(@Param('entity_type') entity_type: 'region' | 'district' | 'city', @Param('entity_id', new ParseIntPipe()) entity_id: number) {
return await this.gisService.getBoundsByEntityTypeAndId(entity_type, entity_id)
}
@Get('/images/all')
async getImages(@Query('offset') offset: number, @Query('limit') limit: number, @Query('city_id') city_id: number) {
return await this.gisService.getImages(offset, limit, city_id)
}
@Get('/figures/all')
async getFigures(@Query('offset') offset: number, @Query('limit') limit: number, @Query('year') year: number, @Query('city_id') city_id: number) {
return await this.gisService.getFigures(offset, limit, year, city_id)
}
@Get('/lines/all')
async getLines(@Query('year') year: number, @Query('city_id') city_id: number) {
return await this.gisService.getLines(year, city_id)
}
@Get('/regions/borders')
async getRegionBorders() {
return await this.gisService.getRegionBorders()
}
}

View File

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { GisController } from './gis.controller';
import { GisService } from './gis.service';
@Module({
controllers: [GisController],
providers: [GisService]
})
export class GisModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { GisService } from './gis.service';
describe('GisService', () => {
let service: GisService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [GisService],
}).compile();
service = module.get<GisService>(GisService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,110 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
@Injectable()
export class GisService {
constructor(
@InjectDataSource('sqliteConnection')
private dataSource: DataSource,
@InjectDataSource('emsConnection')
private emsDataSource: DataSource
) { }
async getTypeRoles(): Promise<any[]> {
const result = await this.emsDataSource.query(`
SELECT * FROM New_Gis..TypeRoles;
`)
return result
}
async getBoundsByEntityType(entity_type: 'region' | 'district' | 'city'): Promise<any[]> {
const result = await this.dataSource.query(`
SELECT entity_id, entity_type, geometry FROM bounds
WHERE entity_type = $1
`, [entity_type])
if (Array.isArray(result)) {
if (result.length > 0) {
const geometries = result.map((bound: { id: string, entity_id: number, entity_type: string, geometry: string, published_at: string, deleted_at: string | null }) => {
return {
...(JSON.parse(bound.geometry)),
properties: {
id: bound.id,
entity_id: bound.entity_id,
entity_type: bound.entity_type
}
}
})
return geometries
} else {
throw new NotFoundException('not found')
}
} else {
throw new NotFoundException('not found')
}
}
async getBoundsByEntityTypeAndId(entity_type: 'region' | 'district' | 'city', entity_id: number): Promise<any[]> {
const result = await this.dataSource.query(`
SELECT entity_id, entity_type, geometry FROM bounds
WHERE entity_type = $1
AND entity_id = $2
`, [entity_type, entity_id])
if (Array.isArray(result)) {
if (result.length > 0) {
return JSON.parse(result[0].geometry)
} else {
throw new NotFoundException('not found')
}
} else {
throw new NotFoundException('not found')
}
}
async getImages(offset: number, limit: number, city_id: number): Promise<any[]> {
const result = await this.emsDataSource.query(`
SELECT * FROM images
${city_id ? `WHERE city_id = ${city_id}` : ''}
ORDER BY city_id
OFFSET ${Number(offset) || 0} ROWS
FETCH NEXT ${Number(limit) || 10} ROWS ONLY;
`)
return result
}
async getFigures(offset: number, limit: number, year: number, city_id: number): Promise<any[]> {
const result = await this.emsDataSource.query(`
SELECT * FROM figures f
JOIN nGeneral..vObjects o ON f.object_id = o.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<any[]> {
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};
`
)
return result
}
async getRegionBorders(): Promise<any[]> {
const result = await this.emsDataSource.query(
`
SELECT * FROM New_Gis..visual_regions
`
)
return result
}
}

View File

@ -0,0 +1,12 @@
export interface SatelliteMapsProviders {
google: 'google';
yandex: 'yandex';
}
export type SatelliteMapsProvider = SatelliteMapsProviders[keyof SatelliteMapsProviders]
export interface Coordinate {
x: number,
y: number
}
export type Extent = [number, number, number, number]

23
server/src/main.ts Normal file
View File

@ -0,0 +1,23 @@
import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
cors: true
});
app.enableCors()
app.useGlobalPipes(new ValidationPipe({ transform: true }))
const config = new DocumentBuilder()
.setTitle('Fuel API')
.setDescription('API test')
.setVersion('0.1')
.addTag('test')
.build()
const documentFactory = () => SwaggerModule.createDocument(app, config)
SwaggerModule.setup('docs', app, documentFactory)
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

View File

@ -0,0 +1,52 @@
import { ApiProperty } from "@nestjs/swagger"
import { IsNumberString } from "class-validator"
export class UploadDTO {
@ApiProperty()
@IsNumberString()
extentMinX: number
@ApiProperty()
@IsNumberString()
extentMinY: number
@ApiProperty()
@IsNumberString()
extentMaxX: number
@ApiProperty()
@IsNumberString()
extentMaxY: number
@ApiProperty()
@IsNumberString()
blX: number
@ApiProperty()
@IsNumberString()
blY: number
@ApiProperty()
@IsNumberString()
tlX: number
@ApiProperty()
@IsNumberString()
tlY: number
@ApiProperty()
@IsNumberString()
trX: number
@ApiProperty()
@IsNumberString()
trY: number
@ApiProperty()
@IsNumberString()
brX: number
@ApiProperty()
@IsNumberString()
brY: number
}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { TilesController } from './tiles.controller';
describe('TilesController', () => {
let controller: TilesController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [TilesController],
}).compile();
controller = module.get<TilesController>(TilesController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,82 @@
import { Body, Controller, Get, NotFoundException, Param, ParseEnumPipe, ParseIntPipe, Post, StreamableFile, UploadedFile, UseInterceptors } from '@nestjs/common';
import { TilesService } from './tiles.service';
import { join } from 'path';
import { createReadStream, existsSync, mkdirSync, writeFileSync } from 'node:fs';
import axios from 'axios';
import { dirname } from 'node:path';
import { FileInterceptor } from '@nestjs/platform-express';
import { UploadDTO } from './dto/upload';
type TileProvider = 'google' | 'yandex' | 'custom'
const tileFolder = join(__dirname, '..', '..', '..', 'storage', 'tile_data')
const uploadDir = join(__dirname, '..', '..', '..', 'storage', 'temp')
@Controller('tiles')
export class TilesController {
constructor(private readonly tileService: TilesService) { }
@Post('/upload')
@UseInterceptors(FileInterceptor('file', {
dest: '../../storage/temp'
}))
async uploadFile(
@UploadedFile() file: Express.Multer.File,
@Body() body: UploadDTO
) {
//await this.tileService.processUpload(file, body)
return { message: 'Uploaded successfully', path: file.path }
}
@Get('/tile/:provider/:z/:x/:y')//✅
async getTile(@Param('provider') provider: TileProvider, @Param('z', new ParseIntPipe()) z: string, @Param('x', new ParseIntPipe()) x: string, @Param('y', new ParseIntPipe()) y: string) {
const tilePath = provider === 'custom' ? join(tileFolder, provider, z.toString(), x.toString(), `${y}.png`) : join(tileFolder, provider, z.toString(), x.toString(), `${y}.jpg`)
if (existsSync(tilePath)) {
const file = createReadStream(tilePath)
return new StreamableFile(file, {
type: 'image/jpeg',
})
} else {
if (provider !== 'custom') {
try {
const tileData = await this.tileService.fetchTileFromAPI(provider, z, x, y)
mkdirSync(dirname(tilePath), { recursive: true })
writeFileSync(tilePath, tileData)
return new StreamableFile(tileData, {
type: 'image/jpeg',
})
} catch (error) {
return error
}
} else {
throw new NotFoundException(`Tile is not generated or not provided`)
}
}
}
@Get('/static/:city_id')
async getStatic(@Param('city_id', new ParseIntPipe()) city_id: number) {
const staticFolder = join(__dirname, '..', '..', '..', 'storage', 'static')
const tilePath1 = join(staticFolder, `${city_id}.jpg`)
const tilePath2 = join(staticFolder, `${city_id}.png`)
if (existsSync(tilePath1)) {
const file = createReadStream(tilePath1)
return new StreamableFile(file, {
type: 'image/jpeg',
})
} else if (existsSync(tilePath2)) {
const file = createReadStream(tilePath2)
return new StreamableFile(file, {
type: 'image/png',
})
} else {
throw new NotFoundException(`Static image for city_id = ${city_id} is not provided`)
}
}
}

View File

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { TilesController } from './tiles.controller';
import { TilesService } from './tiles.service';
@Module({
controllers: [TilesController],
providers: [TilesService]
})
export class TilesModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { TilesService } from './tiles.service';
describe('TilesService', () => {
let service: TilesService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [TilesService],
}).compile();
service = module.get<TilesService>(TilesService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,65 @@
import { Injectable, Logger } from '@nestjs/common';
import { UploadDTO } from './dto/upload';
import { generateTilesForZoomLevel } from './utils/tiles';
import { join } from 'path';
import axios from 'axios';
import { randomUUID } from 'crypto';
export interface Coordinate {
x: number,
y: number
}
const tileFolder = join(__dirname, '..', '..', '..', 'storage', 'tile_data')
const uploadDir = join(__dirname, '..', '..', '..', 'storage', 'temp')
@Injectable()
export class TilesService {
async fetchTileFromAPI(provider: string, z: string, x: string, y: string): Promise<Buffer> {
const url = provider === 'google'
? `https://khms2.google.com/kh/v=984?x=${x}&y=${y}&z=${z}`
: `https://core-sat.maps.yandex.net/tiles?l=sat&x=${x}&y=${y}&z=${z}&scale=1&lang=ru_RU`
const response = await axios.get(url, { responseType: 'arraybuffer' })
return response.data
}
async processUpload(file: Express.Multer.File, body: UploadDTO) {
const {
extentMinX,
extentMinY,
extentMaxX,
extentMaxY,
blX,
blY,
tlX,
tlY,
trX,
trY,
brX,
brY
} = body
const bottomLeft: Coordinate = { x: blX, y: blY }
const topLeft: Coordinate = { x: tlX, y: tlY }
const topRight: Coordinate = { x: trX, y: trY }
const bottomRight: Coordinate = { x: brX, y: brY }
Logger.log(`generating to ${uploadDir} ${tileFolder} ${randomUUID().toString()}`)
if (file) {
for (let z = 0; z <= 21; z++) {
await generateTilesForZoomLevel(
uploadDir,
tileFolder,
file,
[extentMinX, extentMinY, extentMaxX, extentMaxY],
bottomLeft,
topLeft,
topRight,
bottomRight,
z,
)
}
}
}
}

View File

@ -0,0 +1,169 @@
import { Logger } from "@nestjs/common"
import { existsSync, mkdirSync } from "fs"
import { join } from "path"
import * as sharp from 'sharp'
import { epsg3857extent } from "src/constants/ems"
import { Coordinate, Extent } from "src/interfaces/map"
function getTilesPerSide(zoom: number) {
return Math.pow(2, zoom)
}
function normalize(value: number, min: number, max: number) {
return (value - min) / (max - min)
}
function getTileIndex(normalized: number, tilesPerSide: number) {
return Math.floor(normalized * tilesPerSide)
}
function getGridCellPosition(x: number, y: number, extent: Extent, zoom: number) {
const tilesPerSide = getTilesPerSide(zoom)
const minX = extent[0]
const minY = extent[1]
const maxX = extent[2]
const maxY = extent[3]
const xNormalized = normalize(x, minX, maxX)
const yNormalized = normalize(y, minY, maxY)
const tileX = getTileIndex(xNormalized, tilesPerSide)
const tileY = getTileIndex(1 - yNormalized, tilesPerSide)
return { tileX, tileY }
}
function calculateRotationAngle(bottomLeft: Coordinate, bottomRight: Coordinate) {
const deltaX = bottomRight.x - bottomLeft.x
const deltaY = bottomRight.y - bottomLeft.y
const angle = -Math.atan2(deltaY, deltaX)
return angle
}
function roundUpToNearest(number: number, mod: number) {
return Math.floor(number / mod) * mod
}
export async function generateTilesForZoomLevel(uploadDir: string, tileFolder: string, file: Express.Multer.File, polygonExtent: Extent, bottomLeft: Coordinate, topLeft: Coordinate, topRight: Coordinate, bottomRight: Coordinate, zoomLevel: number) {
const angleDegrees = calculateRotationAngle(bottomLeft, bottomRight) * 180 / Math.PI
const { tileX: blX, tileY: blY } = getGridCellPosition(bottomLeft.x, bottomLeft.y, epsg3857extent, zoomLevel)
const { tileX: tlX, tileY: tlY } = getGridCellPosition(topLeft.x, topLeft.y, epsg3857extent, zoomLevel)
const { tileX: trX, tileY: trY } = getGridCellPosition(topRight.x, topRight.y, epsg3857extent, zoomLevel)
const { tileX: brX, tileY: brY } = getGridCellPosition(bottomRight.x, topRight.y, epsg3857extent, zoomLevel)
const minX = Math.min(blX, tlX, trX, brX)
const maxX = Math.max(blX, tlX, trX, brX)
const minY = Math.min(blY, tlY, trY, brY)
const maxY = Math.max(blY, tlY, trY, brY)
const mapWidth = Math.abs(epsg3857extent[0] - epsg3857extent[2])
const mapHeight = Math.abs(epsg3857extent[1] - epsg3857extent[3])
const tilesH = Math.sqrt(Math.pow(4, zoomLevel))
const tileWidth = mapWidth / (Math.sqrt(Math.pow(4, zoomLevel)))
const tileHeight = mapHeight / (Math.sqrt(Math.pow(4, zoomLevel)))
let minPosX = minX - (tilesH / 2)
let maxPosX = maxX - (tilesH / 2) + 1
let minPosY = -(minY - (tilesH / 2))
let maxPosY = -(maxY - (tilesH / 2) + 1)
const newMinX = tileWidth * minPosX
const newMaxX = tileWidth * maxPosX
const newMinY = tileHeight * maxPosY
const newMaxY = tileHeight * minPosY
const paddingLeft = Math.abs(polygonExtent[0] - newMinX)
const paddingRight = Math.abs(polygonExtent[2] - newMaxX)
const paddingTop = Math.abs(polygonExtent[3] - newMaxY)
const paddingBottom = Math.abs(polygonExtent[1] - newMinY)
const pixelWidth = Math.abs(minX - (maxX + 1)) * 256
const pixelHeight = Math.abs(minY - (maxY + 1)) * 256
const width = Math.abs(newMinX - newMaxX)
try {
let perPixel = width / pixelWidth
// constraint to original image width
Logger.log("sharping step 0")
Logger.log("initializing pixel paddings")
const paddingLeftPixel = paddingLeft / perPixel
const paddingRightPixel = paddingRight / perPixel
const paddingTopPixel = paddingTop / perPixel
const paddingBottomPixel = paddingBottom / perPixel
const boundsWidthPixel = Math.abs(polygonExtent[0] - polygonExtent[2]) / perPixel
const boundsHeightPixel = Math.abs(polygonExtent[1] - polygonExtent[3]) / perPixel
Logger.log("initializing pixel paddings")
if (!existsSync(join(tileFolder, 'custom', zoomLevel.toString()))) {
mkdirSync(join(tileFolder, 'custom', zoomLevel.toString()), { recursive: true })
Logger.log('created folder custom')
}
Logger.log(`sharping step 1 ${join(uploadDir, file.filename)}`)
const initialZoomImage = await sharp(join(uploadDir, file.filename))
.rotate(Math.ceil(angleDegrees), {
background: '#00000000'
})
.resize({
width: Math.ceil(boundsWidthPixel),
height: Math.ceil(boundsHeightPixel),
background: '#00000000'
})
.extend({
top: Math.ceil(paddingTopPixel),
left: Math.ceil(paddingLeftPixel),
bottom: Math.ceil(paddingBottomPixel),
right: Math.ceil(paddingRightPixel),
background: '#00000000'
})
.toFormat('png')
.toBuffer({ resolveWithObject: true })
Logger.log('sharping step 2')
if (initialZoomImage) {
await sharp(initialZoomImage.data.buffer)
.resize({
width: roundUpToNearest(Math.ceil(boundsWidthPixel) + Math.ceil(paddingLeftPixel) + Math.ceil(paddingRightPixel), Math.abs(minX - (maxX + 1))),
height: roundUpToNearest(Math.ceil(boundsHeightPixel) + Math.ceil(paddingTopPixel) + Math.ceil(paddingBottomPixel), Math.abs(minY - (maxY + 1))),
})
.toFile(join(tileFolder, 'custom', zoomLevel.toString(), zoomLevel.toString() + '.png'))
.then(async (res) => {
let left = 0
for (let x = minX; x <= maxX; x++) {
if (!existsSync(join(tileFolder, 'custom', zoomLevel.toString(), x.toString()))) {
mkdirSync(join(tileFolder, 'custom', zoomLevel.toString(), x.toString()), { recursive: true });
}
let top = 0
for (let y = minY; y <= maxY; y++) {
console.log(`z: ${zoomLevel} x: ${x} y: ${y}`)
try {
await sharp(join(tileFolder, 'custom', zoomLevel.toString(), zoomLevel.toString() + '.png'))
.extract({
width: res.width / Math.abs(minX - (maxX + 1)),
height: res.height / Math.abs(minY - (maxY + 1)),
left: left,
top: top
})
.toFile(join(tileFolder, 'custom', zoomLevel.toString(), x.toString(), y.toString() + '.png'))
.then(() => {
top = top + res.height / Math.abs(minY - (maxY + 1))
})
} catch (error) {
console.log(error)
}
}
left = left + res.width / Math.abs(minX - (maxX + 1))
}
})
}
} catch (error) {
console.log(error)
}
}

View File

@ -0,0 +1,25 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { App } from 'supertest/types';
import { AppModule } from '../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication<App>;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
});

View File

@ -0,0 +1,9 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}

View File

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

21
server/tsconfig.json Normal file
View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2023",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"noImplicitAny": false,
"strictBindCallApply": false,
"noFallthroughCasesInSwitch": false
}
}