diff --git a/.docker/nginx_dev.conf b/.docker/nginx_dev.conf index cb1af6a..831922b 100644 --- a/.docker/nginx_dev.conf +++ b/.docker/nginx_dev.conf @@ -16,7 +16,7 @@ server { include fastcgi_params; fastcgi_pass backend:9000; # Nom du service PHP-FPM fastcgi_index index.php; - fastcgi_param SCRIPT_FILENAME /app/public/index.php; # Peut être à modifié + fastcgi_param SCRIPT_FILENAME /opt/gpe-api/public/index.php; # Peut être à modifié # try_files $uri /index.php$is_args$args; # Redirige vers index.php si le fichier n'existe pas } diff --git a/package-lock.json b/package-lock.json old mode 100644 new mode 100755 index f489714..0635a65 --- a/package-lock.json +++ b/package-lock.json @@ -17,9 +17,12 @@ "@angular/platform-browser-dynamic": "^18.2.0", "@angular/router": "^18.2.0", "@edugouvfr/ngx-dsfr": "^1.11.9", + "jszip": "^3.10.1", "ol": "^10.2.1", "ol-ext": "^4.0.24", + "proj4": "^2.15.0", "rxjs": "~7.8.0", + "shpjs": "^6.1.0", "tslib": "^2.3.0", "zone.js": "~0.14.10" }, @@ -29,6 +32,8 @@ "@angular/compiler-cli": "^18.2.0", "@types/jasmine": "~5.1.0", "@types/ol-ext": "npm:@siedlerchr/types-ol-ext@^3.5.0", + "@types/proj4": "^2.5.6", + "@types/shapefile": "^0.6.4", "jasmine-core": "~5.2.0", "karma": "~6.4.0", "karma-chrome-launcher": "^3.1.0", @@ -4392,6 +4397,13 @@ "@types/send": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", @@ -4471,6 +4483,13 @@ "jspdf": "^2.5.1" } }, + "node_modules/@types/proj4": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@types/proj4/-/proj4-2.5.6.tgz", + "integrity": "sha512-zfMrPy9fx+8DchqM0kIUGeu2tTVB5ApO1KGAYcSGFS8GoqRIkyL41xq2yCx/iV3sOLzo7v4hEgViSLTiPI1L0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/qs": { "version": "6.9.16", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz", @@ -4540,6 +4559,17 @@ "@types/send": "*" } }, + "node_modules/@types/shapefile": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@types/shapefile/-/shapefile-0.6.4.tgz", + "integrity": "sha512-xZubzHAy4n/OQo32u4l8qx8OGVe9nG258otq4389npOKwXbTXoQSVfqUqKVVrgeR/+FfJt/YSweiVE4kqLqFTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*", + "@types/node": "*" + } + }, "node_modules/@types/sockjs": { "version": "0.3.36", "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", @@ -5901,6 +5931,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/but-unzip": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/but-unzip/-/but-unzip-0.1.4.tgz", + "integrity": "sha512-Q5/55MTk0PHjxtYyZBbhIVMJP0+FNc/AOKBrrnqaxnbJR4I7w+R4CMRNYMxUQrKmCLrih7D1p4/nwZHMn7IToA==", + "license": "Apache-2.0" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -6648,7 +6684,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, "license": "MIT" }, "node_modules/cors": { @@ -8823,6 +8858,12 @@ "node": ">=0.10.0" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/immutable": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", @@ -8883,7 +8924,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/ini": { @@ -9158,7 +9198,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, "license": "MIT" }, "node_modules/isbinaryfile": { @@ -9486,6 +9525,54 @@ "html2canvas": "^1.0.0-rc.5" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/karma": { "version": "6.4.4", "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", @@ -9978,6 +10065,15 @@ } } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/limiter": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", @@ -10529,6 +10625,12 @@ "node": ">= 0.6" } }, + "node_modules/mgrs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mgrs/-/mgrs-1.0.0.tgz", + "integrity": "sha512-awNbTOqCxK1DBGjalK3xqWIstBZgN6fxsMSiXLs9/spqWkF2pAhb2rrYCFSsr1/tT7PhcDGjZndG8SWYn0byYA==", + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -11893,6 +11995,12 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parsedbf": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parsedbf/-/parsedbf-2.0.0.tgz", + "integrity": "sha512-WNjKn/cwgGBkXqQLif+2VMEahcRHkBRU0/RfBWZ7Vj7snRNNW63yW1mVuuHRDyXTRxuGCzAHHBcr/Fn+U/bXjQ==", + "license": "MIT" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -12245,9 +12353,18 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, "license": "MIT" }, + "node_modules/proj4": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/proj4/-/proj4-2.15.0.tgz", + "integrity": "sha512-LqCNEcPdI03BrCHxPLj29vsd5afsm+0sV1H/O3nTDKrv8/LA01ea1z4QADDMjUqxSXWnrmmQDjqFm1J/uZ5RLw==", + "license": "MIT", + "dependencies": { + "mgrs": "1.0.0", + "wkt-parser": "^1.4.0" + } + }, "node_modules/promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -13246,6 +13363,12 @@ "node": ">= 0.4" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -13299,6 +13422,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/shpjs": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/shpjs/-/shpjs-6.1.0.tgz", + "integrity": "sha512-uaUpod7uIWetJK80yiiedZ3x4z9ZAPgDVT89N27+8F97Z8ZOqmu88P96I6CBC8N+YyERqdneZNT/wNFUEnzNpw==", + "license": "MIT", + "dependencies": { + "but-unzip": "^0.1.4", + "parsedbf": "^2.0.0", + "proj4": "^2.1.4" + } + }, "node_modules/side-channel": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", @@ -14458,7 +14592,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, "license": "MIT" }, "node_modules/utils-merge": { @@ -15486,6 +15619,12 @@ "dev": true, "license": "MIT" }, + "node_modules/wkt-parser": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/wkt-parser/-/wkt-parser-1.4.0.tgz", + "integrity": "sha512-qpwO7Ihds/YYDTi1aADFTI1Sm9YC/tTe3SHD24EeIlZxy7Ik6a1b4HOz7jAi0xdUAw487duqpo8OGu+Tf4nwlQ==", + "license": "MIT" + }, "node_modules/wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", diff --git a/package.json b/package.json old mode 100644 new mode 100755 index 2e7562e..d12776c --- a/package.json +++ b/package.json @@ -20,9 +20,12 @@ "@angular/platform-browser-dynamic": "^18.2.0", "@angular/router": "^18.2.0", "@edugouvfr/ngx-dsfr": "^1.11.9", + "jszip": "^3.10.1", "ol": "^10.2.1", "ol-ext": "^4.0.24", + "proj4": "^2.15.0", "rxjs": "~7.8.0", + "shpjs": "^6.1.0", "tslib": "^2.3.0", "zone.js": "~0.14.10" }, @@ -32,6 +35,8 @@ "@angular/compiler-cli": "^18.2.0", "@types/jasmine": "~5.1.0", "@types/ol-ext": "npm:@siedlerchr/types-ol-ext@^3.5.0", + "@types/proj4": "^2.5.6", + "@types/shapefile": "^0.6.4", "jasmine-core": "~5.2.0", "karma": "~6.4.0", "karma-chrome-launcher": "^3.1.0", diff --git a/src/app/requete/pages/requete-new/requete-new.component.html b/src/app/requete/pages/requete-new/requete-new.component.html index 7d910fe..0d05637 100644 --- a/src/app/requete/pages/requete-new/requete-new.component.html +++ b/src/app/requete/pages/requete-new/requete-new.component.html @@ -16,11 +16,42 @@

-
-
- +
+
+ +
+
+
+ + +
+

+ Format de fichier non supporté. Format supporté zips/geojsons/shapefile(shp, prj, shx, dbf). +

+
+
+ + + + + + +
diff --git a/src/app/requete/pages/requete-new/requete-new.component.spec.ts b/src/app/requete/pages/requete-new/requete-new.component.spec.ts index 93d1457..de24bff 100644 --- a/src/app/requete/pages/requete-new/requete-new.component.spec.ts +++ b/src/app/requete/pages/requete-new/requete-new.component.spec.ts @@ -1,5 +1,6 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; import { provideRouter } from '@angular/router'; +import { of } from 'rxjs'; import { RequeteNewComponent } from './requete-new.component'; import { RequeteStepperComponent } from '../../components/requete-stepper/requete-stepper.component'; @@ -8,12 +9,24 @@ import { MapViewerComponent } from '../../../shared-map/components/map-viewer/ma import { ThematicTabsComponent } from '../../../shared-thematic/components/thematic-tabs/thematic-tabs.component'; import { SharedDesignDsfrModule } from '../../../shared-design-dsfr/shared-design-dsfr.module'; import { GeolocaliseFormComponent } from '../../../shared-map/components/geolocalise-form/geolocalise-form.component'; +import { MapContextService } from '../../../shared-map/services/map-context.service'; +import { LocalStorageForetService } from '../../../shared/services/local-storage-foret.service'; +import { ActivatedRoute } from '@angular/router'; describe('RequeteNewComponent', () => { let component: RequeteNewComponent; let fixture: ComponentFixture; + let mapContextService: jasmine.SpyObj; + let localStorageForetService: jasmine.SpyObj; + let activatedRouteStub: Partial; beforeEach(async () => { + mapContextService = jasmine.createSpyObj('MapContextService', ['getLayerDessin', 'resetDessin', 'addDrawingTools', 'updateLayers']); + localStorageForetService = jasmine.createSpyObj('LocalStorageForetService', ['setForet']); + activatedRouteStub = { + data: of({ data: { name: 'Forêt Test', geometry: { type: 'FeatureCollection', features: [] } } }) + }; + await TestBed.configureTestingModule({ declarations: [ RequeteNewComponent, @@ -23,10 +36,11 @@ describe('RequeteNewComponent', () => { MapViewerComponent, ThematicTabsComponent ], - imports: [ - SharedDesignDsfrModule - ], + imports: [SharedDesignDsfrModule], providers: [ + { provide: MapContextService, useValue: mapContextService }, + { provide: LocalStorageForetService, useValue: localStorageForetService }, + { provide: ActivatedRoute, useValue: activatedRouteStub }, provideRouter([]) ] }).compileComponents(); @@ -39,4 +53,68 @@ describe('RequeteNewComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); -}); + + it('should load forest from route data', () => { + expect(component.foret).toBeDefined(); + expect(component.foret?.name).toEqual('Forêt Test'); + }); + + it('should go to next step when features are present', () => { + component.step = 0; + mapContextService.getLayerDessin.and.returnValue({ + getSource: () => ({ getFeatures: () => [{ id: 1 }] }) + }); + + component.nextStep(); + expect(component.step).toBe(1); + }); + + it('should not advance if no features are drawn', () => { + component.step = 0; + mapContextService.getLayerDessin.and.returnValue({ + getSource: () => ({ getFeatures: () => [] }) + }); + + spyOn(window, 'alert'); + component.nextStep(); + + expect(component.step).toBe(0); + expect(window.alert).toHaveBeenCalledWith( + "Veuillez préciser le périmètre de votre forêt à l'aide des outils de dessins disponible sur la carte." + ); + }); + + it('should go back to previous step', () => { + component.step = 1; + component.previousStep(); + + expect(component.step).toBe(0); + expect(mapContextService.resetDessin).toHaveBeenCalled(); + expect(mapContextService.addDrawingTools).toHaveBeenCalled(); + expect(mapContextService.updateLayers).toHaveBeenCalled(); + }); + + + it('should upload and process a GeoJSON file', fakeAsync(async () => { + const mockFile = new File([JSON.stringify({ type: 'FeatureCollection', features: [] })], 'test.geojson', { type: 'application/json' }); + + spyOn(component as any, 'readFileAsText').and.returnValue(Promise.resolve(mockFile.text())); + spyOn(component as any, 'reprojectGeoJson').and.returnValue({ type: 'FeatureCollection', features: [] }); + + const event = { target: { files: [mockFile] } } as unknown as Event; + await component.uploadContour(event); + + expect(component.fileFormatError).toBeFalse(); + expect(mapContextService.getLayerDessin).toHaveBeenCalled(); + })); + + it('should show error for invalid GeoJSON file', fakeAsync(async () => { + const mockFile = new File(['{invalid json}'], 'test.geojson', { type: 'application/json' }); + spyOn(component as any, 'readFileAsText').and.returnValue(Promise.resolve('{invalid json}')); + + const event = { target: { files: [mockFile] } } as unknown as Event; + await component.uploadContour(event); + + expect(component.fileFormatError).toBeTrue(); + })); +}); \ No newline at end of file diff --git a/src/app/requete/pages/requete-new/requete-new.component.ts b/src/app/requete/pages/requete-new/requete-new.component.ts index 25f6950..d975f79 100644 --- a/src/app/requete/pages/requete-new/requete-new.component.ts +++ b/src/app/requete/pages/requete-new/requete-new.component.ts @@ -7,6 +7,19 @@ import { BreadcrumbTransformerService } from '../../../shared-design-dsfr/transf import { THEMATIC_LIST } from '../../../shared-thematic/models/thematic-list.enum'; import { Foret } from '../../../shared/models/foret.model'; import { LocalStorageForetService } from '../../../shared/services/local-storage-foret.service'; +import GeoJSON from 'ol/format/GeoJSON'; + +import shp from 'shpjs'; +import JSZip from 'jszip'; +import proj4 from 'proj4'; +import { transform, get as getProjection, ProjectionLike } from 'ol/proj'; + +// Définition d'alias pour EPSG:3857 +proj4.defs("EPSG:3857", "+proj=merc +lon_0=0 +k=1 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs"); + + +import { HttpClient } from '@angular/common/http'; + @Component({ selector: 'app-requete-new', @@ -21,6 +34,8 @@ export class RequeteNewComponent implements OnInit, AfterViewInit { breadcrumb?: any; + fileFormatError: boolean = false; + constructor( private breadcrumbTransformerService: BreadcrumbTransformerService, private localStorageForetService: LocalStorageForetService, @@ -124,4 +139,232 @@ export class RequeteNewComponent implements OnInit, AfterViewInit { }); } + /** + * Gestion des fichiers importés (GeoJSON, ZIP, Shapefile) + */ + async uploadContour(event: Event): Promise { + const input = event.target as HTMLInputElement; + if (!input.files || input.files.length === 0) return; + + const files = Array.from(input.files); + const geoJsons: any[] = []; + const shpFiles: { [key: string]: Blob } = {}; // Stocker les fichiers Shapefile + + for (const file of files) { + try { + const fileExtension = file.name.split('.').pop()?.toLowerCase(); + + if (fileExtension === 'geojson') { + let geoJson = await this.readFileAsText(file); + geoJson = this.reprojectGeoJson(JSON.parse(geoJson), 'EPSG:3857'); + geoJsons.push(geoJson); + + } else if (fileExtension === 'zip') { + const extractedGeoJsons = await this.handleZipFile(file); + const reprojectedGeoJsons = extractedGeoJsons.map(gj => this.reprojectGeoJson(gj, 'EPSG:3857')); + geoJsons.push(...reprojectedGeoJsons); + + } else if (['shp', 'dbf', 'shx', 'prj'].includes(fileExtension!)) { + shpFiles[file.name] = file; + } + + } catch (error) { + //console.error(`Erreur lors du traitement du fichier ${file.name} :`, error); + this.fileFormatError = true; + } + } + + // Si on a des fichiers Shapefile valides (shp, dbf, shx, prj), on les traite + if (Object.keys(shpFiles).some(name => name.endsWith('.shp'))) { + const shapefileGeoJsonArray = await this.handleShpFiles(shpFiles); + const reproShapefileGeoJsonArray = shapefileGeoJsonArray.map(gj => this.reprojectGeoJson(gj, 'EPSG:3857')); + geoJsons.push(...reproShapefileGeoJsonArray); + } + + + if (geoJsons.length > 0) { + this.fileFormatError = false; + const mergedGeoJson = this.mergeGeoJsons(geoJsons); + this.mapContextService.maForetFromGeoJson(mergedGeoJson); + this.mapContextService.centerOnDessin(); + } else { + this.fileFormatError = true; + } + } + + + /** + * Gestion des fichiers ZIP contenant des GeoJSON et/ou des Shapefiles. + */ + private async handleZipFile(file: File): Promise { + try { + const zip = await JSZip.loadAsync(file); + const geoJsons: any[] = []; + const shpFiles: { [key: string]: Blob } = {}; + + for (const fileName of Object.keys(zip.files)) { + const file = zip.files[fileName]; + if (file.dir) continue; + + const fileExtension = fileName.split('.').pop()?.toLowerCase(); + + if (fileExtension === 'geojson') { + const geoJsonContent = await file.async('string'); + geoJsons.push(JSON.parse(geoJsonContent)); + + } else if (['shp', 'dbf', 'shx', 'prj'].includes(fileExtension!)) { + shpFiles[fileName] = await file.async('blob'); + } + } + + // Si on a des fichiers Shapefile valides, on les convertit + if (Object.keys(shpFiles).some(name => name.endsWith('.shp'))) { + const shapefileGeoJson = await this.handleShpFiles(shpFiles); + geoJsons.push(...shapefileGeoJson); + } + + return geoJsons; + + } catch (error) { + //console.error("Erreur lors du traitement du fichier ZIP :", error); + return []; + } + } + + /** + * Convertit un ensemble de fichiers Shapefile en GeoJSON. + */ + private async handleShpFiles(shpFiles: { [key: string]: Blob }): Promise { + try { + const zip = new JSZip(); + for (const [fileName, blob] of Object.entries(shpFiles)) { + zip.file(fileName, blob); + } + + const zipArrayBuffer = await zip.generateAsync({ type: 'arraybuffer' }); + const shapefileGeoJson = await shp(zipArrayBuffer); + + return Array.isArray(shapefileGeoJson) ? shapefileGeoJson : [shapefileGeoJson]; + + } catch (error) { + //console.error("Erreur lors de la conversion du Shapefile :", error); + return []; + } + } + + /** + * Lecture d'un fichier texte (GeoJSON). + */ + private readFileAsText(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result as string); + reader.onerror = reject; + reader.readAsText(file); + }); + } + + /** + * Fusionne plusieurs fichiers GeoJSON en un seul FeatureCollection. + */ + private mergeGeoJsons(geoJsons: any[]): any { + return { + type: 'FeatureCollection', + features: geoJsons.flatMap((geoJson) => geoJson.features || []), + }; + } + + /** + * Reprojette un GeoJSON d'une projection inconnue vers la projection cible (par défaut : EPSG:3857) + * @param geoJson - Le GeoJSON à reprojeter + * @param targetProj - La projection cible (ex: 'EPSG:3857') + * @returns GeoJSON reprojeté + */ + private reprojectGeoJson(geoJson: any, targetProj: string = 'EPSG:3857'): any { + if (!geoJson || geoJson.type !== 'FeatureCollection') return geoJson; + + const sourceProj = this.detectGeoJsonProjection(geoJson) || 'EPSG:4326'; // Si non trouvé, on suppose WGS84 + + return { + ...geoJson, + features: geoJson.features.map((feature: GeoJSON.Feature) => { + // Vérifie si `feature.geometry` est un objet avec `coordinates` + if (feature.geometry && 'coordinates' in feature.geometry) { + return { + ...feature, + geometry: { + ...feature.geometry, + coordinates: this.reprojectCoordinates( + feature.geometry.coordinates, + feature.geometry.type, + sourceProj, + targetProj + ) + } + }; + } else { + // Si c'est un `GeometryCollection`, on ne modifie pas + return feature; + } + }) + }; + } + + + + /** + * Détecte la projection d'un GeoJSON en se basant sur les métadonnées + * @param geoJson - L'objet GeoJSON + * @returns La projection détectée (ex: 'EPSG:4326') ou undefined si inconnue + */ + private detectGeoJsonProjection(geoJson: any): string | undefined { + // Cas où le CRS est explicitement défini dans le GeoJSON + if (geoJson.crs && geoJson.crs.properties && geoJson.crs.properties.name) { + return geoJson.crs.properties.name; + } + + // Si le fichier provient d'un Shapefile, il peut y avoir un fichier PRJ + if (geoJson.proj4) { + const epsgCode = this.getEpsgFromProj4(geoJson.proj4); + return epsgCode ? `EPSG:${epsgCode}` : undefined; + } + + return undefined; // On ne peut pas déterminer la projection + } + + /** + * Transforme les coordonnées d'un GeoJSON (Point, LineString, Polygon, MultiPolygon...) + */ + private reprojectCoordinates(coordinates: any, type: string, sourceProj: string, targetProj: string): any { + if (!getProjection(sourceProj) || !getProjection(targetProj)) { + //console.warn(`Projection inconnue : ${sourceProj} ou ${targetProj}. Aucune transformation appliquée.`); + return coordinates; + } + + switch (type) { + case 'Point': + return transform(coordinates, sourceProj, targetProj); + case 'LineString': + case 'MultiPoint': + return coordinates.map((coord: [number, number]) => transform(coord, sourceProj, targetProj)); + case 'Polygon': + case 'MultiLineString': + return coordinates.map((ring: Array<[number, number]>) => ring.map((coord: [number, number]) => transform(coord, sourceProj, targetProj))); + case 'MultiPolygon': + return coordinates.map((polygon: Array>) => polygon.map((ring: Array<[number, number]>) => ring.map((coord: [number, number]) => transform(coord, sourceProj, targetProj)))); + + default: + return coordinates; + } + } + + /** + * Extrait le code EPSG à partir d'une définition Proj4 + * @param proj4String - Chaîne de caractères Proj4 + * @returns Code EPSG (ex: 4326) ou undefined si non trouvé + */ + private getEpsgFromProj4(proj4String: string): number | undefined { + const match = proj4String.match(/EPSG:(\d+)/); + return match ? parseInt(match[1], 10) : undefined; + } } diff --git a/src/types/proj4.d.ts b/src/types/proj4.d.ts new file mode 100644 index 0000000..efe529a --- /dev/null +++ b/src/types/proj4.d.ts @@ -0,0 +1,4 @@ +declare module 'proj4' { + const proj4: any; + export default proj4; +} \ No newline at end of file diff --git a/src/types/shpjs.d.ts b/src/types/shpjs.d.ts new file mode 100644 index 0000000..7b2d658 --- /dev/null +++ b/src/types/shpjs.d.ts @@ -0,0 +1,8 @@ +declare module 'shpjs' { + const shp: { + (file: string | Blob | ArrayBuffer): Promise; + parseZip(buffer: ArrayBuffer): Promise; + read(buffer: ArrayBuffer): Promise; + }; + export default shp; +} diff --git a/tsconfig.app.json b/tsconfig.app.json index 3775b37..ed8bef4 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -4,6 +4,7 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", + "typeRoots": ["node_modules/@types", "src/types"], "types": [] }, "files": [