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": [