diff --git a/.eslintrc b/.eslintrc
index 27a0c22c4..783ae46d9 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -45,10 +45,7 @@
"no-console": "off",
"no-proto": "off",
"no-prototype-builtins": "off",
- "linebreak-style": [
- "error",
- "unix"
- ],
+ "linebreak-style": "off",
"padded-blocks": [
"error",
{
diff --git a/build/webpack/controls.webpack.config.js b/build/webpack/controls.webpack.config.js
index 99544a143..38247e5e7 100644
--- a/build/webpack/controls.webpack.config.js
+++ b/build/webpack/controls.webpack.config.js
@@ -74,6 +74,14 @@ module.exports = (env, argv) => {
// formats
break;
case "SearchEngine":
+ case "SearchEngineBase":
+ case "SearchEngineGeocodeIGN":
+ case "SearchEngineAdvanced":
+ case "LocationAdvancedSearch":
+ case "ParcelAdvancedSearch":
+ case "AbstractAdvancedSearch":
+ case "InseeAdvancedSearch":
+ case "CoordinateAdancedSearch":
// crs
break;
case "MeasureArea":
diff --git a/build/webpack/extend.themes.webpack.js b/build/webpack/extend.themes.webpack.js
index fb030783f..4ccc5f627 100644
--- a/build/webpack/extend.themes.webpack.js
+++ b/build/webpack/extend.themes.webpack.js
@@ -28,6 +28,7 @@ module.exports = (env, argv) => {
path.join(rootdir, "src", "packages", "CSS", "Controls/ReverseGeocoding", "GPFreverseGeocodingStyle.css"),
path.join(rootdir, "src", "packages", "CSS", "Controls/Route", "GPFrouteStyle.css"),
path.join(rootdir, "src", "packages", "CSS", "Controls/SearchEngine", "GPFsearchEngineStyle.css"),
+ path.join(rootdir, "src", "packages", "CSS", "Controls/SearchEngine", "GPFadvancedSearchEngineStyle.css"),
path.join(rootdir, "src", "packages", "CSS", "Controls/ToolBoxMeasure", "GPFtoolBoxMeasureStyle.css"),
path.join(rootdir, "src", "packages", "CSS", "Controls/Zoom", "GPFzoomStyle.css"),
path.join(rootdir, "src", "packages", "CSS", "Controls/FullScreen", "GPFfullScreenStyle.css"),
@@ -64,6 +65,7 @@ module.exports = (env, argv) => {
path.join(rootdir, "src", "packages", "CSS", "Controls/ReverseGeocoding", "DSFRreverseGeocodingStyle.css"),
path.join(rootdir, "src", "packages", "CSS", "Controls/Route", "DSFRrouteStyle.css"),
path.join(rootdir, "src", "packages", "CSS", "Controls/SearchEngine", "DSFRsearchEngineStyle.css"),
+ path.join(rootdir, "src", "packages", "CSS", "Controls/SearchEngine", "DSFRadvancedSearchEngineStyle.css"),
path.join(rootdir, "src", "packages", "CSS", "Controls/ToolBoxMeasure", "DSFRtoolBoxMeasureStyle.css"),
path.join(rootdir, "src", "packages", "CSS", "Controls/Zoom", "DSFRzoomStyle.css"),
path.join(rootdir, "src", "packages", "CSS", "Controls/FullScreen", "DSFRfullScreenStyle.css"),
diff --git a/build/webpack/modules.webpack.config.js b/build/webpack/modules.webpack.config.js
index 0cc800678..b5043c583 100644
--- a/build/webpack/modules.webpack.config.js
+++ b/build/webpack/modules.webpack.config.js
@@ -45,6 +45,14 @@ module.exports = (env, argv) => {
"GpfExtOlReverseGeocode" : path.join(rootdir, "src", "packages", "Controls/ReverseGeocode", "ReverseGeocode.js"),
"GpfExtOlRoute" : path.join(rootdir, "src", "packages", "Controls/Route", "Route.js"),
"GpfExtOlSearchEngine" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "SearchEngine.js"),
+ "GpfExtOlSearchEngineBase" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "SearchEngineBase.js"),
+ "GpfExtOlSearchEngineGeocodeIGN" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "SearchEngineGeocodeIGN.js"),
+ "GpfExtOlSearchEngineAdvanced" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "SearchEngineAdvanced.js"),
+ "GpfExtOlLocationAdvancedSearch" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "LocationAdvancedSearch.js"),
+ "GpfExtOlParcelAdvancedSearch" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "ParcelAdvancedSearch.js"),
+ "GpfExtOlInseeAdvancedSearch" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "InseeAdvancedSearch.js"),
+ "GpfExtOlAbstractAdvancedSearch" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "AbstractAdvancedSearch.js"),
+ "GpfExtOlCoordinateAdvancedSearch" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "CoordinateAdvancedSearch.js"),
"GpfExtOlExport" : path.join(rootdir, "src", "packages", "Controls/Export", "Export.js"),
"GpfExtOlMeasureArea" : path.join(rootdir, "src", "packages", "Controls", "Measures", "MeasureArea.js"),
"GpfExtOlMeasureAzimuth" : path.join(rootdir, "src", "packages", "Controls", "Measures", "MeasureAzimuth.js"),
@@ -58,6 +66,9 @@ module.exports = (env, argv) => {
"GpfExtOlControlList" : path.join(rootdir, "src", "packages", "Controls/ControlList", "ControlList.js"),
"GpfExtOlContextMenu" : path.join(rootdir, "src", "packages", "Controls/ContextMenu", "ContextMenu.js"),
"GpfExtOlReporting" : path.join(rootdir, "src", "packages", "Controls/Reporting", "Reporting.js"),
+ // Interactions
+ "GpfExtOlDrawingInteraction" : path.join(rootdir, "src", "packages", "Interactions/Drawing.js"),
+ "GpfExtOlSelectingInteraction" : path.join(rootdir, "src", "packages", "Interactions/Selecting.js"),
// Formats étendus
"GpfExtOlFormats" : [
path.join(rootdir, "src", "packages", "Formats", "GeoJSON.js"),
diff --git a/demos/angular-project/angular.json b/demos/angular-project/angular.json
index fc1bbbf42..26faa96e5 100644
--- a/demos/angular-project/angular.json
+++ b/demos/angular-project/angular.json
@@ -38,6 +38,7 @@
"node_modules/geopf-extensions-openlayers/src/packages/CSS/Controls/MousePosition/GPFmousePosition.css",
"node_modules/geopf-extensions-openlayers/src/packages/CSS/Controls/Export/GPFexport.css",
"node_modules/geopf-extensions-openlayers/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css",
+ "node_modules/geopf-extensions-openlayers/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css",
"node_modules/geopf-extensions-openlayers/src/packages/CSS/Controls/Editor/GPFeditor.css",
"node_modules/geopf-extensions-openlayers/src/packages/CSS/Controls/ReverseGeocoding/GPFreverseGeocoding.css",
"node_modules/geopf-extensions-openlayers/src/packages/CSS/Controls/Drawing/GPFdrawing.css",
diff --git a/samples-src/pages/tests/DrawingInteraction/pages-ol-drawinginteraction-modules-default.html b/samples-src/pages/tests/DrawingInteraction/pages-ol-drawinginteraction-modules-default.html
new file mode 100644
index 000000000..66c8264e1
--- /dev/null
+++ b/samples-src/pages/tests/DrawingInteraction/pages-ol-drawinginteraction-modules-default.html
@@ -0,0 +1,100 @@
+{{#extend "ol-sample-modules-dsfr-layout"}}
+
+{{#content "vendor"}}
+
+
+
+{{/content}}
+
+{{#content "head"}}
+
Sample openlayers Drawing interaction
+{{/content}}
+
+{{#content "style"}}
+
+{{/content}}
+
+{{#content "body"}}
+ Outil de dessin avec style
+
+
+
+ Dernière sélection :
+
+
+
+{{/content}}
+
+{{#content "js"}}
+
+{{/content}}
+
+{{/extend}}
diff --git a/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-default.html b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-default.html
new file mode 100644
index 000000000..9fe1e54a5
--- /dev/null
+++ b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-default.html
@@ -0,0 +1,88 @@
+{{#extend "ol-sample-modules-layout"}}
+
+{{#content "vendor"}}
+
+
+
+{{/content}}
+
+{{#content "head"}}
+ Sample openlayers SearchEngine
+{{/content}}
+
+{{#content "style"}}
+
+{{/content}}
+
+{{#content "body"}}
+ Ajout du moteur de recherche avec les options par défaut
+
+
+
+ Dernière sélection :
+{{/content}}
+
+{{#content "js"}}
+
+{{/content}}
+
+{{/extend}}
diff --git a/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html
new file mode 100644
index 000000000..5341c5045
--- /dev/null
+++ b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html
@@ -0,0 +1,121 @@
+{{#extend "ol-sample-modules-dsfr-layout"}}
+
+{{#content "vendor"}}
+
+
+
+
+
+
+
+
+{{/content}}
+
+{{#content "head"}}
+ Sample openlayers SearchEngine
+{{/content}}
+
+{{#content "style"}}
+
+{{/content}}
+
+{{#content "body"}}
+ Ajout du moteur de recherche avec les options par défaut
+
+
+
+ Dernière sélection :
+
+{{/content}}
+
+{{#content "js"}}
+
+{{/content}}
+
+{{/extend}}
diff --git a/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeIGN.html b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeIGN.html
new file mode 100644
index 000000000..53f08fe21
--- /dev/null
+++ b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeIGN.html
@@ -0,0 +1,79 @@
+{{#extend "ol-sample-modules-dsfr-layout"}}
+
+{{#content "vendor"}}
+
+
+
+{{/content}}
+
+{{#content "head"}}
+ Sample openlayers SearchEngine
+{{/content}}
+
+{{#content "style"}}
+
+{{/content}}
+
+{{#content "body"}}
+ Ajout du moteur de recherche avec les options par défaut
+
+
+
+ Dernière sélection :
+{{/content}}
+
+{{#content "js"}}
+
+{{/content}}
+
+{{/extend}}
diff --git a/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-geocodeIGN.html b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-geocodeIGN.html
new file mode 100644
index 000000000..a86fc6c0e
--- /dev/null
+++ b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-geocodeIGN.html
@@ -0,0 +1,86 @@
+{{#extend "ol-sample-modules-layout"}}
+
+{{#content "vendor"}}
+
+
+
+{{/content}}
+
+{{#content "head"}}
+ Sample openlayers SearchEngine
+{{/content}}
+
+{{#content "style"}}
+
+{{/content}}
+
+{{#content "body"}}
+ Ajout du moteur de recherche avec les options par défaut
+
+
+
+ Dernière sélection :
+{{/content}}
+
+{{#content "js"}}
+
+{{/content}}
+
+{{/extend}}
diff --git a/src/index.js b/src/index.js
index 0c45507be..02d4da920 100644
--- a/src/index.js
+++ b/src/index.js
@@ -24,6 +24,14 @@ export { default as LayerMapBox } from "./packages/Layers/LayerMapBox";
export { default as LayerSwitcher } from "./packages/Controls/LayerSwitcher/LayerSwitcher";
export { default as GetFeatureInfo } from "./packages/Controls/GetFeatureInfo/GetFeatureInfo";
export { default as SearchEngine } from "./packages/Controls/SearchEngine/SearchEngine";
+export { default as SearchEngineBase } from "./packages/Controls/SearchEngine/SearchEngineBase";
+export { default as SearchEngineAdvanced } from "./packages/Controls/SearchEngine/SearchEngineAdvanced";
+export { default as SearchEngineGeocodeIGN } from "./packages/Controls/SearchEngine/SearchEngineGeocodeIGN";
+export { default as AbstractAdvancedSearch } from "./packages/Controls/SearchEngine/AbstractAdvancedSearch";
+export { default as CoordinateAdvancedSearch } from "./packages/Controls/SearchEngine/CoordinateAdvancedSearch";
+export { default as InseeAdvancedSearch } from "./packages/Controls/SearchEngine/InseeAdvancedSearch";
+export { default as LocationAdvancedSearch } from "./packages/Controls/SearchEngine/LocationAdvancedSearch";
+export { default as ParcelAdvancedSearch } from "./packages/Controls/SearchEngine/ParcelAdvancedSearch";
export { default as MousePosition } from "./packages/Controls/MousePosition/MousePosition";
export { default as Drawing } from "./packages/Controls/Drawing/Drawing";
export { default as Route } from "./packages/Controls/Route/Route";
@@ -48,6 +56,17 @@ export { default as ControlList } from "./packages/Controls/ControlList/ControlL
export { default as ContextMenu } from "./packages/Controls/ContextMenu/ContextMenu";
export { default as Reporting } from "./packages/Controls/Reporting/Reporting";
+
+// Interactions
+export { default as DrawingInteraction } from "./packages/Interactions/Drawing";
+export { default as SelectingInteraction } from "./packages/Interactions/Selecting";
+
+// Services
+export { default as AbstractSearchService } from "./packages/Services/AbstractSearchService";
+export { default as DefaultSearchService } from "./packages/Services/DefaultSearchService";
+export { default as IGNSearchService } from "./packages/Services/IGNSearchService";
+export { default as InseeSearchService } from "./packages/Services/InseeSearchService";
+
// proj4
export { default as Proj4 } from "proj4";
diff --git a/src/packages/CSS/Controls/SearchEngine/DSFRadvancedSearchEngineStyle.css b/src/packages/CSS/Controls/SearchEngine/DSFRadvancedSearchEngineStyle.css
new file mode 100644
index 000000000..ae7fb8821
--- /dev/null
+++ b/src/packages/CSS/Controls/SearchEngine/DSFRadvancedSearchEngineStyle.css
@@ -0,0 +1,133 @@
+/** STYLE DSFR RECHERCHE AVANCEE **/
+
+div[data-open="true"] > section.fr-accordion::before {
+ box-shadow: inset 0 1px 0 0 var(--border-default-grey);
+}
+
+
+div[id^=GPsearchEngine-Advanced] {
+ width: 100%;
+}
+
+@media (min-width: 36em) {
+ div[id^=GPsearchEngine-Advanced] {
+ width: 360px;
+ }
+}
+
+div[id^=GPsearchEngine-AdvancedContainer] > button.GPSearchEngine-locate {
+ margin-bottom : 0.75rem;
+}
+
+form[id^=GPAdvancedForm-CoordinateAdvancedSearch] > .GPLonLatInputs {
+ display: flex;
+ flex-direction: row;
+ gap: 1rem;
+}
+
+form[id^=GPAdvancedForm-CoordinateAdvancedSearch][data-unit="DMS"] > .GPLonLatInputs {
+ flex-direction: column;
+}
+
+form[id^=GPAdvancedForm-CoordinateAdvancedSearch] > .GPLonLatInputs .display-mask {
+ display: none;
+}
+
+form[id^=GPAdvancedForm-CoordinateAdvancedSearch][data-unit="DMS"] > .GPLonLatInputs .display-mask {
+ display: unset;
+ position: absolute;
+ top: 0.5rem;
+ left: 1rem;
+ pointer-events: none;
+ font-family: inherit;
+}
+
+form[id^=GPAdvancedForm-CoordinateAdvancedSearch][data-unit="DMS"] > .GPLonLatInputs input[id^=CoordinateAdvancedSearch-] {
+ color: transparent;
+ caret-color: var(--text-default-grey);
+ letter-spacing: 2px;
+}
+
+
+form[id^=GPAdvancedForm-CoordinateAdvancedSearch] > .GPLonLatInputs select {
+ display: none;
+}
+
+form[id^=GPAdvancedForm-CoordinateAdvancedSearch][data-unit="DMS"] > .GPLonLatInputs select {
+ display: unset;
+}
+
+form[id^=GPAdvancedForm-CoordinateAdvancedSearch] > .GPLonLatInputs .GPCoordinateInputs {
+ display: flex;
+ flex-direction: row;
+ gap: 8px;
+}
+
+
+form[id^=GPAdvancedForm-CoordinateAdvancedSearch] > .GPLonLatInputs .GPCoordinateInputs select {
+ width:auto;
+}
+
+.GPCoordinateInputs > div {
+ position: relative;
+}
+
+
+[id^="GPsearchEngine-Advanced"] > .GPAdvancedContainer {
+ background-color: var(--background-default-grey);
+}
+
+[id^="GPsearchEngine-Advanced"] .GPsearchInputSubmit:disabled {
+ background-color: var(--background-action-high-blue-france);
+}
+
+/* Surcharge DSFR */
+div[id^=GPsearchEngine-AdvancedContainer-] .fr-accordion .fr-collapse {
+ margin: 0 -0.25rem;
+ padding-left: 1rem;
+ padding-right: 1rem;
+}
+
+.fr-icon-map-pin-add-line::before,
+.fr-icon-map-pin-add-line::after {
+ -webkit-mask-image: url("./img/dsfr/map-pin-add-line.svg");
+ mask-image: url("./img/dsfr/map-pin-add-line.svg");
+}
+.fr-icon-map-pin-add-fill::before,
+.fr-icon-map-pin-add-fill::after {
+ -webkit-mask-image: url("./img/dsfr/map-pin-add-fill.svg");
+ mask-image: url("./img/dsfr/map-pin-add-fill.svg");
+}
+
+.fr-icon-bike-line::before,
+.fr-icon-bike-line::after {
+ -webkit-mask-image: url("./img/dsfr/bike-line.svg");
+ mask-image: url("./img/dsfr/bike-line.svg");
+}
+
+.fr-icon-parking-box-line::before,
+.fr-icon-parking-box-line::after {
+ -webkit-mask-image: url("./img/dsfr/parking-box-line.svg");
+ mask-image: url("./img/dsfr/parking-box-line.svg");
+}
+.fr-icon-plane-line::before,
+.fr-icon-plane-line::after {
+ -webkit-mask-image: url("./img/dsfr/plane-line.svg");
+ mask-image: url("./img/dsfr/plane-line.svg");
+}
+.fr-icon-subway-line::before,
+.fr-icon-subway-line::after {
+ -webkit-mask-image: url("./img/dsfr/subway-line.svg");
+ mask-image: url("./img/dsfr/subway-line.svg");
+}
+
+[id^="GPsearchEngine"] form[id^="GPsearchInput-Base-"].GPSearchBar > .GPInputGroup:has(> .GPsearchInputText:focus:focus-visible) {
+ outline-offset: 2px;
+ outline-width: 2px;
+ outline-color: #0a76f6;
+ outline-style: solid;
+}
+
+[id^="GPsearchEngine"] form[id^="GPsearchInput-Base-"].GPSearchBar > .GPInputGroup >.GPsearchInputText:focus:focus-visible {
+ outline-style: none;
+}
\ No newline at end of file
diff --git a/src/packages/CSS/Controls/SearchEngine/DSFRsearchEngineStyle.css b/src/packages/CSS/Controls/SearchEngine/DSFRsearchEngineStyle.css
index 51af56796..2cee62301 100644
--- a/src/packages/CSS/Controls/SearchEngine/DSFRsearchEngineStyle.css
+++ b/src/packages/CSS/Controls/SearchEngine/DSFRsearchEngineStyle.css
@@ -1,3 +1,12 @@
+
+/* Mode Mobile */
+@media (min-width: 576px) {
+ [id^="GPsearchEngine-"] {
+ top: 8px;
+ left: 46px;
+ }
+}
+
.gpf-widget-padding {
padding-top: 5px;
padding-bottom: 5px;
@@ -126,16 +135,19 @@ div.GPbuttonsContainer > button {
width: 40px;
}
-form[id^=GPsearchInput-] {
- margin-left: 8px;
- width: 300px;
-}
+/* form[id^=GPsearchInput-] {
+ width: 312px;
+} */
button[id^="GPshowSearchEnginePicto-"][aria-pressed="false"] + form[id^=GPsearchInput-] {
- max-width: 300px;
+ max-width: 0px;
border: none;
}
+button[id^="GPshowSearchEnginePicto-"][aria-pressed="true"] + form[id^=GPsearchInput-] {
+ max-width: 312px;
+}
+
button[id^="GPshowSearchEnginePicto-"] {
border-radius: 0 0.25rem 0 0;
}
@@ -228,6 +240,10 @@ div[id^=GPgeocodeResults-] {
border-bottom: 1px solid var(--grey-900-175);
}
+.GPSearchBar > div.GPInputGroup, .GPSearchBar > div.GPInputGroup > input {
+ box-shadow: inset 0 -2px 0 0 var(--border-action-high-blue-france);
+}
+
/* Mode Mobile */
@media (max-width: 576px) {
@@ -263,3 +279,149 @@ div[id^=GPgeocodeResults-] {
max-height: 4rem;
}
}
+
+[id^="GPsearchEngine"] .GPautoCompleteContainer {
+ background-color: var(--background-default-grey);
+}
+
+[id^="GPsearchEngine"] ul {
+ padding: 0 12px;
+ margin: 0.75rem 0 0;
+ width: 100%;
+}
+
+/* [id^="GPsearchEngine"] form[id^="GPsearchInput-Base-"] .GPOptionsContainer button {
+ font-size: 0.875rem;
+ line-height: 1.5rem;
+ min-height: 2rem;
+ padding: 0.25rem 0.75rem;
+} */
+
+[id^="GPsearchEngine"] ul:empty {
+ padding: unset;
+ margin: unset;
+}
+
+[id^="GPsearchEngine"] ul li {
+ padding: 0.75rem 0.5rem;
+ color: var(--text-action-high-grey);
+ box-shadow: 0 -1px 0 0 var(--border-default-grey);
+}
+
+[id^="GPsearchEngine"] ul li.GPsearchHistoric {
+ padding-right: 1.75rem;
+}
+
+[id^="GPsearchEngine"] ul li.GPsearchHistoric > span {
+ position: absolute;
+ right: 1rem;
+ color: var(--grey-625-425);
+}
+
+[id^="GPsearchEngine"] ul li:first-child {
+ box-shadow: none;
+}
+
+
+[id^="GPsearchEngine"] ul li::before {
+ margin-right: 0.5rem;
+}
+
+[id^="GPsearchEngine"] ul li.active,
+[id^="GPsearchEngine"] ul li:hover {
+ background-color: var(--background-default-grey-hover);
+}
+
+[id^="GPsearchEngine"] ul li:active {
+ background-color: var(--background-default-grey-active);
+}
+
+.fr-icon-history-line::before,
+.fr-icon-history-line::after {
+ -webkit-mask-image: url("./img/dsfr/history-line.svg");
+ mask-image: url("./img/dsfr/history-line.svg");
+}
+
+form[id^=GPsearchInput] > input[id^=GPsearchInputText] {
+ height: 3rem;
+ max-height: 3rem;
+}
+
+.GPSearchPopup {
+ display: flex;
+ position: relative;
+ align-items: center;
+ justify-content: space-between;
+ flex-wrap: nowrap;
+ gap: 12px;
+ width: 288px;
+
+ bottom: 17px;
+ padding: 0.75rem;
+ border: 1px solid var(--border-default-grey);
+ box-shadow: var(--overlap-shadow);
+ background-color: var(--background-default-grey);
+ font-size: 0.75em;
+}
+/* Border layer (slightly larger triangle) */
+.GPSearchPopup::before {
+ content: "";
+ position: absolute;
+ border-top: 16px solid var(--border-default-grey); /* Border color */
+ border-right: 15px solid transparent;
+ border-left: 15px solid transparent;
+ bottom: -17px;
+ margin-left: -15px;
+ left: 50%;
+}
+
+/* Inner layer (actual background triangle) */
+.GPSearchPopup::after {
+ content: "";
+ position: absolute;
+ border-top: 15px solid var(--background-default-grey); /* Same as popup bg */
+ border-right: 14px solid transparent;
+ border-left: 14px solid transparent;
+ bottom: -15px;
+ margin-left: -14px;
+ left: 50%;
+}
+
+.GPPopupContent {
+ line-height: 1.25rem;
+}
+
+.GPButtonGroups {
+ display: flex;
+ flex-direction: row-reverse;
+ flex: 0 0 auto;
+ align-items: center;
+}
+
+form.GPSearchBar > .GPInputGroup,form.GPSearchBar > button[id^=GPshowSearchEnginePicto-].fr-btn, form.GPSearchBar > .GPInputGroup > input {
+ height: 48px;
+ max-height: unset;
+}
+
+form.GPSearchBar > button[id^=GPshowSearchEnginePicto-].fr-btn {
+ width: 48px;
+ max-width: unset;
+}
+
+button[id^="GPSearchEngine-erase-btn"] {
+ display: none;
+}
+
+form.GPSearchBar > .GPInputGroup > input[data-erase] ~ .GPOptionsContainer > button[id^="GPSearchEngine-erase-btn"] {
+ display:block;
+}
+
+form.GPSearchBar > .GPInputGroup > input[data-erase] ~ .GPOptionsContainer > button[id^="GPSearchEngine-advanced-btn"] {
+ display:none;
+}
+
+.fr-icon-ign-mer::before,
+.fr-icon-ign-mer::after {
+ -webkit-mask-image: url("./img/dsfr/ign-mer.svg");
+ mask-image: url("./img/dsfr/ign-mer.svg");
+}
\ No newline at end of file
diff --git a/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css b/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css
new file mode 100644
index 000000000..c8f99e98f
--- /dev/null
+++ b/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css
@@ -0,0 +1,107 @@
+/** STYLE COMMUN RECHERCHE AVANCEE **/
+
+div[id^=GPsearchEngine-Advanced] {
+ padding: 5px;
+ margin: -5px;
+}
+
+div[id^=GPsearchEngine-Advanced] div[id^=GPsearchEngine-] {
+ width: 100%;
+ margin: 0 5px;
+}
+
+form[id^=GPAdvancedForm-], form[id^=GPAdvancedForm-] * {
+ height : 100%;
+ width: 100%;
+}
+
+form[id^=GPAdvancedForm-] > .GPFormFooter {
+ margin-top: 0.75rem;
+ display: flex;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ gap: 16px;
+}
+
+[id^="GPsearchEngine-Advanced"] form[id^="GPsearchInput-Base-"]:first-child {
+ display: flex;
+ flex-direction: row;
+}
+
+form[id^=GPAdvancedForm-] > .GPFormFooter > button {
+ flex: 1 1 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+form[id^=GPAdvancedForm-] ul.search-result {
+ list-style-type: none;
+ padding: 0;
+ margin: -0.5em 0 0;
+}
+form[id^=GPAdvancedForm-] ul.search-result a {
+ text-decoration: none;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ margin: 0;
+ padding: 0;
+ background: none;
+ display: block;
+}
+form[id^=GPAdvancedForm-] ul.search-result a:before {
+ margin-right: 0.5em;
+}
+form[id^=GPAdvancedForm-] ul.search-result .hidden {
+ display: none;
+}
+form[id^=GPAdvancedForm-] ul.search-result button {
+ flex: 1 1 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+form[id^=GPAdvancedForm-] ul.search-result .search-result-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 8px;
+ flex-direction: column;
+ text-align: center;
+}
+form[id^=GPAdvancedForm-] ul.search-result .search-result-actions:hover {
+ background-color: transparent;
+}
+form[id^=GPAdvancedForm-] ul.search-result .fr-message--error {
+ color: var(--background-flat-warning);
+ white-space: normal;
+}
+
+form[id^=GPAdvancedForm-] select option.option {
+ background-color: var(--background-action-low-blue-france);
+}
+
+
+/* Search parcels */
+form[id^=GPAdvancedForm-ParcelAdvancedSearch-] .GPautoCompleteList {
+ display: none;
+ z-index: 1;
+ padding: 0;
+}
+form[id^=GPAdvancedForm-ParcelAdvancedSearch-] input[aria-expanded="true"] ~ .GPautoCompleteList {
+ display: block;
+}
+
+form[id^=GPAdvancedForm-ParcelAdvancedSearch-] .GPautoCompleteList li {
+ height: auto;
+}
+
+form[id^=GPAdvancedForm-ParcelAdvancedSearch-] [id^=ParcelAdvancedSearch-section] option:first-child{
+ color: #aaa;
+ color: var(--text-disabled-grey);
+ font-style: italic;
+}
+form[id^=GPAdvancedForm-ParcelAdvancedSearch-] .fr-select-group:has(select:disabled) label,
+form[id^=GPAdvancedForm-ParcelAdvancedSearch-] .fr-input-group:has(input:disabled) label {
+ color: var(--text-disabled-grey);
+}
+
diff --git a/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngineStyle.css b/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngineStyle.css
new file mode 100644
index 000000000..96ff1301e
--- /dev/null
+++ b/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngineStyle.css
@@ -0,0 +1,5 @@
+/** STYLE NON DSFR RECHERCHE AVANCEE **/
+
+div[id^=GPsearchEngine-Advanced] {
+ width:312px;
+}
\ No newline at end of file
diff --git a/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css b/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css
index eb32455c5..d9e23f8c3 100644
--- a/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css
+++ b/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css
@@ -1,10 +1,5 @@
/* SEARCH ENGINE */
-[id^="GPsearchEngine-"] {
- top: 8px;
- left: 46px;
-}
-
[id^="GPsearchEngine-"] {
/* dsfr */
display: flex;
@@ -62,6 +57,7 @@ form[id^=GPsearchInput-] {
display: inline-block;
position: relative;
overflow: hidden;
+ width: 100%;
transition: max-width 0.5s ease-out 0s;
}
@@ -134,9 +130,6 @@ form[id^=GPadvancedSearchForm]:has(option[value="Coordinates"]:checked) > input[
/* Simple search input */
-[id^="GPsearchInput"] {}
-
-
[id^="GPshowSearchDiv"]{
display: flex;
}
@@ -164,6 +157,7 @@ form[id^=GPadvancedSearchForm]:has(option[value="Coordinates"]:checked) > input[
[id^="GPautoCompleteList"] {
margin-left: 33px;
+ z-index: 2;
}
[id^="GPautoCompleteList"] {
@@ -187,3 +181,127 @@ form[id^=GPadvancedSearchForm]:has(option[value="Coordinates"]:checked) > input[
}
}
+
+[id^="GPsearchEngine"] .GPautoCompleteContainer {
+ /* position: absolute; */
+ box-shadow: 0 2px 6px 0 rgba(0, 0, 18, 0.16);
+ list-style: none;
+ overflow: hidden;
+ flex-direction: column;
+ width: 100%;
+ z-index: 1;
+}
+[id^="GPsearchEngine"] ul li {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+[id^=GPsearchEngine-].ol-collapsed form[id^=GPsearchInput-Base-] {
+ overflow: hidden;
+}
+[id^=GPsearchEngine-] form[id^=GPsearchInput-Base-] {
+ overflow: visible;
+}
+
+[id^="GPsearchEngine"] {
+ display: flex;
+ flex-direction: column;
+ padding: 0;
+}
+
+[id^="GPsearchEngine"] form[id^=GPsearchInput-Base-].GPSearchBar > :first-child {
+ display: flex;
+ flex-direction: row;
+ padding: 0;
+}
+
+[id^="GPsearchEngine"] form[id^=GPsearchInput-Base-] .GPOptionsContainer {
+ display: flex;
+ height: calc(100% - 2px);
+}
+
+[id^="GPsearchEngine"] form[id^=GPsearchInput-Base-] .GPOptionsContainer button {
+ margin: 0.5rem;
+}
+
+.GPautoCompleteHeader {
+ margin-bottom: 0.75rem;
+ padding: 0.75rem 0.75rem 0;
+}
+
+[id^="GPsearchEngine"] form[id^=GPsearchInput-Base-] .GPautoCompleteHeader button {
+ /* margin: 12px 12px 0; */
+ max-width: unset;
+}
+[id^="GPsearchEngine"] form[id^=GPsearchInput-Base-] .GPautoCompleteFooter button {
+ margin: 0 12px 12px;
+ max-width: unset;
+}
+
+[id^="GPsearchEngine"] .GPautoCompleteContainer .GPautoCompleteHeader {
+ display: none;
+}
+[id^="GPsearchEngine"] .GPautoCompleteContainer[data-type="history"] .GPautoCompleteHeader {
+ display: block;
+}
+
+/* Advanced search panel */
+[id^="GPsearchEngine-Advanced"] {
+ display: flex;
+ flex-direction: column;
+ max-height: calc(100% - 2rem);
+ overflow: hidden;
+}
+[id^="GPsearchEngine-Advanced"] [id^="GPsearchEngine-"]{
+ max-height: unset;
+}
+
+[id^="GPsearchEngine-Advanced"] [id^="GPsearchEngine-"] {
+ position: static;
+}
+[id^="GPsearchEngine-Advanced"] > div {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+}
+[id^="GPsearchEngine-Advanced"] > .GPAdvancedContainer {
+ display: none;
+ box-sizing: content-box;
+ max-width: 100%;
+ max-height: unset;
+ padding: 0.75rem 0.75rem 1px;
+ box-sizing: border-box;
+}
+[id^="GPsearchEngine-Advanced"]:has(.GPSearchEngine-advanced-btn[aria-expanded="true"]) > .GPAdvancedContainer {
+ display: flex;
+ z-index: 1;
+}
+[id^="GPsearchEngine-Advanced"] .GPSearchEngine-advanced-btn[aria-expanded="false"]::after {
+ transform: rotate(180deg);
+}
+
+/* [id^="GPsearchEngine-Advanced"] .GPAdvancedContainer[data-open="true"] > button {
+ display: none;
+} */
+
+[id^="GPsearchEngine-Advanced"][data-open="true"] section {
+ overflow: hidden auto;
+}
+/* [id^="GPsearchEngine-Advanced"] [data-open="true"] section::before {
+ display: none;
+} */
+
+[id^="GPsearchEngine-Advanced"] section .fr-collapse {
+ box-sizing: border-box;
+}
+
+[id^="GPsearchEngine-Advanced"] [id^="GPsearchInput-Base-"] .GPsearchInputText {
+ border-radius: 0.25rem 0.25rem 0 0;
+}
+[id^="GPsearchEngine-Advanced"] section [id^="GPsearchInput-Base-"] .GPsearchInputSubmit {
+ display: none;
+}
+[id^="GPsearchEngine-Advanced"] section [id^="GPsearchEngine-"] {
+ margin-top: 0.5rem;
+}
diff --git a/src/packages/CSS/Controls/SearchEngine/GPFsearchEngineStyle.css b/src/packages/CSS/Controls/SearchEngine/GPFsearchEngineStyle.css
index eefaa8fed..7b0e974d5 100644
--- a/src/packages/CSS/Controls/SearchEngine/GPFsearchEngineStyle.css
+++ b/src/packages/CSS/Controls/SearchEngine/GPFsearchEngineStyle.css
@@ -3,6 +3,8 @@
[id^="GPsearchEngine-"] {
flex-direction: row;
align-items: center;
+ top: 8px;
+ left: 46px;
}
.position-container-top-left [id^="GPsearchEngine-"],
@@ -159,10 +161,14 @@ dialog[id^=GPgeocodeResultsList] {
width: 280px;
position: absolute;
max-height: 140px;
- background-color: var(--background-default-grey);
+ background-color: #ffffff;
}
+[id^="GPsearchEngine-Advanced"] > .GPAdvancedContainer {
+ background-color: #ffffff;
+}
+
div[id^=GPautoCompleteList] {
top: 35px;
}
@@ -213,3 +219,24 @@ div[id^=GPgeocodeResults-] {
[id^="GPautocompleteResultsLocation"] {
padding: 0;
}
+
+[id^="GPsearchEngine"] .GPautoCompleteContainer {
+ background-color: white;
+}
+
+[id^="GPsearchEngine"] ul {
+ margin: 3px 0;
+ width: 100%;
+ padding: 0;
+}
+
+[id^="GPsearchEngine"] ul li {
+ color: #5E5E5E;
+ padding: 6px 10px;
+}
+
+[id^="GPsearchEngine"] ul li.active,
+[id^="GPsearchEngine"] ul li:hover {
+ color: #000000;
+ background-color: #CEDBEF;
+}
\ No newline at end of file
diff --git a/src/packages/CSS/Controls/SearchEngine/img/dsfr/bike-line.svg b/src/packages/CSS/Controls/SearchEngine/img/dsfr/bike-line.svg
new file mode 100644
index 000000000..f05c49135
--- /dev/null
+++ b/src/packages/CSS/Controls/SearchEngine/img/dsfr/bike-line.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/packages/CSS/Controls/SearchEngine/img/dsfr/history-line.svg b/src/packages/CSS/Controls/SearchEngine/img/dsfr/history-line.svg
new file mode 100644
index 000000000..91df49d3d
--- /dev/null
+++ b/src/packages/CSS/Controls/SearchEngine/img/dsfr/history-line.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/packages/CSS/Controls/SearchEngine/img/dsfr/ign-mer.svg b/src/packages/CSS/Controls/SearchEngine/img/dsfr/ign-mer.svg
new file mode 100644
index 000000000..7423e9e76
--- /dev/null
+++ b/src/packages/CSS/Controls/SearchEngine/img/dsfr/ign-mer.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/packages/CSS/Controls/SearchEngine/img/dsfr/map-pin-add-fill.svg b/src/packages/CSS/Controls/SearchEngine/img/dsfr/map-pin-add-fill.svg
new file mode 100644
index 000000000..13e803d55
--- /dev/null
+++ b/src/packages/CSS/Controls/SearchEngine/img/dsfr/map-pin-add-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/packages/CSS/Controls/SearchEngine/img/dsfr/map-pin-add-line.svg b/src/packages/CSS/Controls/SearchEngine/img/dsfr/map-pin-add-line.svg
new file mode 100644
index 000000000..4b9c407c7
--- /dev/null
+++ b/src/packages/CSS/Controls/SearchEngine/img/dsfr/map-pin-add-line.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/packages/CSS/Controls/SearchEngine/img/dsfr/parking-box-line.svg b/src/packages/CSS/Controls/SearchEngine/img/dsfr/parking-box-line.svg
new file mode 100644
index 000000000..a27d8b801
--- /dev/null
+++ b/src/packages/CSS/Controls/SearchEngine/img/dsfr/parking-box-line.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/packages/CSS/Controls/SearchEngine/img/dsfr/plane-line.svg b/src/packages/CSS/Controls/SearchEngine/img/dsfr/plane-line.svg
new file mode 100644
index 000000000..7a554dbd5
--- /dev/null
+++ b/src/packages/CSS/Controls/SearchEngine/img/dsfr/plane-line.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/packages/CSS/Controls/SearchEngine/img/dsfr/subway-line.svg b/src/packages/CSS/Controls/SearchEngine/img/dsfr/subway-line.svg
new file mode 100644
index 000000000..81d96f657
--- /dev/null
+++ b/src/packages/CSS/Controls/SearchEngine/img/dsfr/subway-line.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/packages/CSS/Interactions/Drawing.css b/src/packages/CSS/Interactions/Drawing.css
new file mode 100644
index 000000000..89d943816
--- /dev/null
+++ b/src/packages/CSS/Interactions/Drawing.css
@@ -0,0 +1,28 @@
+.ol-control.ol-drawing-info {
+ color: var(--light-decisions-text-text-default-grey, #3A3A3A);
+ /* 2.Corps de texte/XS - Texte mention/Desktop & Mobile - Regular */
+ font-family: Marianne;
+ font-size: 12px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 20px;
+
+ display: inline-flex;
+ padding: 8px 12px;
+ justify-content: center;
+ align-items: center;
+ gap: 4px;
+
+ background-color: rgba(255, 255, 255, 0.8);
+ border-radius: 4px;
+ font-size: 12px;
+
+ top: 4em;
+ left: 50%;
+ transform: translateX(-50%);
+ pointer-events: none!important;
+}
+.ol-control.ol-drawing-info:empty {
+ pointer-events: none!important;
+ display: none;
+}
diff --git a/src/packages/Controls/SearchEngine/AbstractAdvancedSearch.js b/src/packages/Controls/SearchEngine/AbstractAdvancedSearch.js
new file mode 100644
index 000000000..010d37ac1
--- /dev/null
+++ b/src/packages/Controls/SearchEngine/AbstractAdvancedSearch.js
@@ -0,0 +1,189 @@
+// import CSS
+import "../../CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css";
+import Control from "../Control";
+import Logger from "../../Utils/LoggerByDefault";
+import Helper from "../../Utils/Helper";
+import Map from "ol/Map";
+var logger = Logger.getLogger("abstractAdvancedSearch");
+
+// Typedefs partagés disponibles dans ./typedefs.js (AbstractAdvancedSearchOptions, ...)
+
+/**
+ * @classdesc
+ * Contrôle de base pour les recherches avancées (formulaires spécialisés).
+ *
+ * Fournit la structure HTML du formulaire, la gestion des inputs et
+ * les événements d'effacement et de soumission. Les contrôles spécialisés
+ * doivent surcharger addInputs() et _onSearch().
+ *
+ * @alias ol.control.AbstractAdvancedSearch
+ * @module AbstractAdvancedSearch
+*/
+class AbstractAdvancedSearch extends Control {
+
+ /**
+ * @constructor
+ * @param {AbstractAdvancedSearchOptions} options Options du constructeur
+ *
+ */
+ constructor (options) {
+ options = options || {};
+ // call ol.control.Control constructor
+ super(options);
+
+ /**
+ * Nom de la classe (heritage)
+ * @private
+ */
+ this.CLASSNAME = "AbstractAdvancedSearch";
+
+ // initialisation du composant
+ this.initialize(options);
+
+ this.element = this._initContainer(options);
+ this._initEvents(options);
+ }
+
+ setMap (map) {
+ super.setMap(map);
+ this.inputs.forEach(input => {
+ if (input.setMap) {
+ input.setMap(map);
+ }
+ });
+ // Replace les boutons à la fin
+ this.element.appendChild(this.btnGroup);
+ }
+
+ /**
+ * Initialise le contrôle (appelé par le constructeur).
+ * @param {AbstractAdvancedSearchOptions} options Options d'initialisation
+ * @protected
+ */
+ initialize (options) {
+ if (!options.name) {
+ throw new SyntaxError("`name` is mandatory");
+ } else {
+ this.name = options.name;
+ }
+
+ this.inputs = [];
+ }
+
+ /**
+ * Retourne le nom du la recherche avancée
+ * @returns {String} Nom de la recherche avancée
+ */
+ getName () {
+ return this.name;
+ }
+
+ /**
+ * Retourne le formulaire de la recherche avancée
+ * @returns {HTMLFormElement} Formulaire de la recherche
+ */
+ getContent () {
+ return this.element;
+ }
+
+ /**
+ * Crée le conteneur DOM du formulaire.
+ *
+ * @param {AbstractAdvancedSearchOptions} options Options d'initialisation
+ * @returns {HTMLFormElement} Élément du formulaire
+ * @protected
+ */
+ _initContainer (options) {
+ let element = document.createElement("form");
+ element.className = "GPForm gpf-advanced-search-form";
+ element.id = Helper.getUid("GPAdvancedForm-" + this.CLASSNAME + "-");
+
+ let fieldset = document.createElement("fieldset");
+ fieldset.className = "GPFieldset fr-fieldset gpf-advanced-search-fieldset";
+ fieldset.id = Helper.getUid("GPAdvancedFieldset-");
+
+ this.addInputs();
+ this.inputs.forEach((elem) => {
+ if (elem instanceof Control) {
+ elem.setTarget(element);
+ } else {
+ element.appendChild(elem);
+ }
+ });
+
+ // element.appendChild(fieldset);
+
+ const btnGroup = this.btnGroup = document.createElement("div");
+ btnGroup.className = "GPFormFooter";
+
+ const eraseBtn = this.eraseBtn = document.createElement("button");
+ eraseBtn.type = "reset";
+ eraseBtn.className = "GPBtn gpf-btn fr-btn fr-btn--tertiary";
+ eraseBtn.id = Helper.getUid("GPEraseBtn-");
+ eraseBtn.textContent = "Effacer";
+
+ const searchBtn = this.searchBtn = document.createElement("button");
+ searchBtn.className = "GPBtn gpf-btn fr-btn";
+ searchBtn.id = Helper.getUid("GPSearchBtn-");
+ searchBtn.type = "submit";
+ searchBtn.textContent = "Rechercher";
+ searchBtn.setAttribute("form", element.id);
+
+ btnGroup.appendChild(eraseBtn);
+ btnGroup.appendChild(searchBtn);
+
+ element.appendChild(btnGroup);
+
+ return element;
+ }
+
+ /**
+ * Ajoute des éléments d'input dans la collection `this.inputs`.
+ * Cette méthode est abstraite et doit être surchargée par les implémentations spécifiques.
+ * @protected
+ * @abstract
+ */
+ addInputs () {
+
+ }
+
+
+ /** Add event listeners
+ * @param {AbstractAdvancedSearchOptions} options - constructor options
+ * @protected
+ */
+ _initEvents (options) {
+ this.eraseBtn.onclick = this._onErase.bind(this);
+ this.element.onsubmit = this._onSearch.bind(this);
+ }
+
+ /**
+ * Réinitialise les champs du formulaire.
+ * @param {PointerEvent} e Événement d'effacement
+ * @protected
+ */
+ _onErase (e) {
+ e.preventDefault();
+ this.getContent().querySelectorAll("input").forEach(input => {
+ input.value = "";
+ });
+ }
+
+ /**
+ * Traitement lors de la soumission du formulaire (recherche).
+ * Doit être surchargé par les contrôles spécifiques pour lancer la recherche.
+ * @param {SubmitEvent|PointerEvent} e Évènement de soumission / recherche
+ * @protected
+ */
+ _onSearch (e) {
+ e.preventDefault();
+ }
+
+}
+
+export default AbstractAdvancedSearch;
+
+// Expose AbstractAdvancedSearch as ol.control.AbstractAdvancedSearch (for a build bundle)
+if (window.ol && window.ol.control) {
+ window.ol.control.AbstractAdvancedSearch = AbstractAdvancedSearch;
+}
diff --git a/src/packages/Controls/SearchEngine/CoordinateAdvancedSearch.js b/src/packages/Controls/SearchEngine/CoordinateAdvancedSearch.js
new file mode 100644
index 000000000..b5a1b48c5
--- /dev/null
+++ b/src/packages/Controls/SearchEngine/CoordinateAdvancedSearch.js
@@ -0,0 +1,680 @@
+// import CSS
+import AbstractAdvancedSearch from "./AbstractAdvancedSearch";
+import Logger from "../../Utils/LoggerByDefault";
+import CRS from "../../CRS/CRS";
+import Helper from "../../Utils/Helper";
+import {
+ transform as olProjTransform,
+ get as olProjGet
+} from "ol/proj";
+import MathUtils from "../../Utils/MathUtils";
+import Feature from "ol/Feature.js";
+import Point from "ol/geom/Point.js";
+
+let logger = Logger.getLogger("abstractAdvancedSearch");
+
+/**
+ * @classdesc
+ * Contrôle de recherche avancée par coordonnées
+ * (saisie pour différents systèmes de coordonnées et unités correspondantes).
+ *
+ * @alias ol.control.CoordinateAdvancedSearch
+ * @module CoordinateAdvancedSearch
+ */
+class CoordinateAdvancedSearch extends AbstractAdvancedSearch {
+
+ /**
+ * Constructeur.
+ *
+ * @constructor
+ * @param {CoordinateAdvancedSearchOptions} [options] Options du contrôle
+ */
+ constructor (options) {
+ super(options);
+
+ this.element.dataset.unitType = this.get("unitType");
+ this.element.dataset.unit = this.unit.value;
+ }
+
+ /**
+ * @override
+ * @protected
+ * @param {CoordinateAdvancedSearchOptions} options Options d'initialisation
+ */
+ initialize (options) {
+ if (!options.name) {
+ options.name = "Coordonnées";
+ }
+ super.initialize(options);
+
+ this._initCoordinateSearchSystems(options);
+ this._initCoordinateSearchUnits(options);
+
+ this._boundOnLonLatBeforeInput = this._onlonLatBeforeInput.bind(this);
+ this._boundOnLonLatInput = this._onlonLatInput.bind(this);
+
+ this._currentCoordinateSystem = this._coordinateSearchSystems[0];
+ this._currentUnit = this._coordinateSearchUnits[this._currentCoordinateSystem.type];
+
+ this.set("unitType", this._currentCoordinateSystem.type);
+ this.set("unit", this._currentUnit[0].code);
+
+ /**
+ * Nom de la classe (heritage)
+ * @private
+ */
+ this.CLASSNAME = "CoordinateAdvancedSearch";
+ }
+
+ /**
+ * @private
+ * @param {CoordinateAdvancedSearchOptions} [options] Options d'initialisation possibles (options.coordinateSearch.systems)
+ */
+ _initCoordinateSearchSystems (options) {
+ this._coordinateSearchSystems = [];
+ // on donne la possibilité à l'utilisateur de modifier
+ // la liste des systèmes à afficher
+ // Ex. this.options.coordinateSearch.systems
+
+ // systemes de projection disponible par defaut
+ let projectionSystemsByDefault = [{
+ label : "G\u00e9ographique",
+ crs : "EPSG:4326",
+ type : "Geographical"
+ }, {
+ label : "Web Mercator",
+ crs : "EPSG:3857",
+ type : "Metric"
+ }, {
+ label : "Lambert 93",
+ crs : "EPSG:2154",
+ type : "Metric"
+ }];
+
+ if (options && options.coordinateSearch && options.coordinateSearch.systems) {
+ let systems = options.coordinateSearch.systems;
+ // on ajoute les definitions d'un systeme de reference fournies par l'utilisateur
+ for (let i = 0; i < systems.length; i++) {
+ let sys = systems[i];
+ this._setSystem(sys);
+ }
+ }
+
+ // on ajoute les systèmes de projections par défaut
+ if (this._coordinateSearchSystems.length === 0) {
+ for (let j = 0; j < projectionSystemsByDefault.length; j++) {
+ this._setSystem(projectionSystemsByDefault[j]);
+ }
+ }
+ }
+
+ /**
+ * @private
+ * @param {CoordinateAdvancedSearchOptions} [options] Options d'initialisation possibles (options.coordinateSearch.units)
+ */
+ _initCoordinateSearchUnits (options) {
+ this._coordinateSearchUnits = [];
+ // on donne la possibilité à l'utilisateur de modifier
+ // la liste des unités à afficher
+ // Ex.
+ // this.options.units : ["DEC", "DMS"]
+
+ // unités disponible par defaut
+ let projectionUnitsByDefault = {
+ Geographical : [{
+ code : "DEC",
+ label : "Degrés décimaux",
+ format : MathUtils.coordinateToDecimal
+ }, {
+ code : "DMS",
+ label : "Degrés sexagésimaux",
+ format : MathUtils.coordinateToDMS
+ }],
+ Metric : [{
+ code : "M",
+ label : "Mètres",
+ format : MathUtils.coordinateToMeter
+ }, {
+ code : "KM",
+ label : "Kilomètres",
+ format : MathUtils.coordinateToKMeter
+ }]
+ };
+
+ if (options && options.coordinateSearch && options.coordinateSearch.units) {
+ let units = options.coordinateSearch.units;
+ for (let type in projectionUnitsByDefault) {
+ if (projectionUnitsByDefault.hasOwnProperty(type)) {
+ let found = false;
+ for (let j = 0; j < projectionUnitsByDefault[type].length; j++) {
+ let obj = projectionUnitsByDefault[type][j];
+ for (let i = 0; i < units.length; i++) {
+ let unit = units[i];
+ if (obj.code === unit) {
+ found = true;
+ if (!this._coordinateSearchUnits[type]) {
+ this._coordinateSearchUnits[type] = [];
+ }
+ this._coordinateSearchUnits[type].push(obj);
+ }
+ }
+ }
+ if (!found) {
+ this._coordinateSearchUnits[type] = projectionUnitsByDefault[type];
+ }
+ }
+ }
+ }
+
+ // au cas où...
+ if (typeof this._coordinateSearchUnits === "object" && Object.keys(this._coordinateSearchUnits).length === 0) {
+ this._coordinateSearchUnits = projectionUnitsByDefault;
+ }
+ }
+
+ /**
+ * Définit un système de projection supplémentaire et charge sa définition CRS si nécessaire.
+ *
+ * @private
+ * @param {CoordinateSearchSystem} system Description du système de projection
+ * @returns {void}
+ */
+ _setSystem (system) {
+ if (typeof system !== "object") {
+ logger.log("[ERROR] CoordinateAdvancedSearch:_setSystem - system parameter should be an object");
+ return;
+ }
+ if (!system.crs) {
+ logger.error("crs not defined !");
+ return;
+ }
+ if (!system.label) {
+ logger.warn("crs label not defined, use crs code by default.");
+ system.label = system.crs;
+ }
+ if (!system.type) {
+ logger.warn("type srs not defined, use 'Metric' by default.");
+ system.type = "Metric";
+ }
+
+ // chargement de la definition de la projection
+ // même si déjà chargé...
+ CRS.loadByName(system.crs);
+
+ if (!olProjGet(system.crs)) {
+ logger.error("crs '{}' not available into proj4 definitions !", system.crs);
+ return;
+ }
+
+ // add system to control systems
+ for (var j = 0; j < this._coordinateSearchSystems.length; j++) {
+ var obj = this._coordinateSearchSystems[j];
+ if (system.crs === obj.crs) {
+ // warn user
+ logger.info("crs '{}' already configured", obj.crs);
+ }
+ }
+ system.code = this._coordinateSearchSystems.length;
+ this._coordinateSearchSystems.push(system);
+ }
+
+ /**
+ * Crée un conteneur d'étiquette pour un élément d'input.
+ *
+ * @private
+ * @param {String} text Texte de l'étiquette
+ * @param {String} type Classe CSS du conteneur
+ * @param {HTMLElement} [input] Élément input à rattacher
+ * @param {Boolean} [mandatory=false] Indique si le champ est obligatoire
+ * @returns {HTMLElement} Élément conteneur (div)
+ */
+ _getLabelContainer (text, type, input, mandatory = false) {
+ const container = document.createElement("div");
+ container.className = type;
+ const label = this._createLabel(text, mandatory);
+ container.appendChild(label);
+ if (input) {
+ label.setAttribute("for", input.id);
+ container.appendChild(input);
+ }
+ return container;
+ }
+
+ /**
+ * Crée un élément label.
+ *
+ * @private
+ * @param {String} text Texte du label
+ * @param {Boolean} [mandatory=false] Ajoute un astérisque si vrai
+ * @returns {HTMLLabelElement} Élément label créé
+ */
+ _createLabel (text, mandatory = false) {
+ const label = document.createElement("label");
+ label.className = "fr-label";
+ const star = mandatory ? "*" : "";
+ label.textContent = text + star;
+ return label;
+ }
+
+ /**
+ * Met à jour le texte d'un label existant.
+ *
+ * @private
+ * @param {String} text Nouveau texte
+ * @param {HTMLElement} container Conteneur contenant le label à mettre à jour
+ * @param {Boolean} [mandatory=false] Indique si le champ est obligatoire
+ */
+ _updateLabel (text, container, mandatory = false) {
+ const label = container.querySelector("label");
+ const star = mandatory ? "*" : "";
+ label.innerText = text + star;
+ }
+
+ /**
+ * @override
+ * @protected
+ */
+ addInputs () {
+ super.addInputs();
+
+ // Indication champs obligatoire
+ const div = document.createElement("div");
+ const mandatory = document.createElement("span");
+ mandatory.className = "GPLabelHint fr-hint-text";
+ mandatory.textContent = "* Tous les champs sont obligatoires";
+ div.appendChild(mandatory);
+ this.inputs.push(div);
+
+ // Système de référence
+ const systemSelect = this.system = document.createElement("select");
+ systemSelect.className = "fr-select";
+ systemSelect.id = systemSelect.name = Helper.getUid("CoordinateAdvancedSearch-system-");
+ const systemContainer = this._getLabelContainer("Système de référence", "fr-select-group", systemSelect, true);
+ this.inputs.push(systemContainer);
+
+ this._coordinateSearchSystems.forEach((elem, index) => {
+ const option = document.createElement("option");
+ option.value = index;
+ option.selected = !index;
+ option.innerText = elem.label;
+ systemSelect.appendChild(option);
+ });
+
+ // Unité correspondantes
+ const unitSelect = this.unit = document.createElement("select");
+ unitSelect.className = "fr-select";
+ unitSelect.id = unitSelect.name = Helper.getUid("CoordinateAdvancedSearch-unit-");
+ const unitContainer = this._getLabelContainer("Unité", "fr-select-group", unitSelect, true);
+ this.inputs.push(unitContainer);
+
+ for (let j = 0; j < this._currentUnit.length; j++) {
+ const unit = this._currentUnit[j];
+ const option = document.createElement("option");
+ option.value = (unit.code) ? unit.code : j;;
+ option.textContent = unit.label || j;
+ unitSelect.appendChild(option);
+ }
+
+ // Longitude et Latitude / X et Y
+ const values = this.lonLatInputs = document.createElement("div");
+ values.className = "GPLonLatInputs";
+
+ // Longitude / X
+ const lonWrapper = this.lon = this.createCoordinateInput("lon");
+ values.appendChild(lonWrapper);
+
+ // Latitude / Y
+ const latWrapper = this.lat = this.createCoordinateInput("lat");
+ values.appendChild(latWrapper);
+
+ this.inputs.push(values);
+ }
+
+ /**
+ * Crée le wrapper (label + input + masques) pour une composante de coordonnées (lon/lat).
+ *
+ * @private
+ * @param {"lon"|"lat"} type Type de composante ("lon" ou "lat")
+ * @returns {HTMLElement} Wrapper contenant l'input et éléments associés
+ */
+ createCoordinateInput (type) {
+ // type = "lon" or "lat"
+ const wrapper = document.createElement("div");
+ wrapper.className = "GPCoordinateWrapper";
+
+ // Label
+ const labelText = type === "lon" ? "Longitude" : "Latitude";
+ const label = this._createLabel(labelText, true);
+ wrapper.appendChild(label);
+
+ // Input container
+ const inputContainer = document.createElement("div");
+ inputContainer.className = "GPCoordinateInputs";
+ wrapper.appendChild(inputContainer);
+
+ // Input
+ const div = document.createElement("div");
+ const input = document.createElement("input");
+ input.className = "GPCoordinateInput fr-input";
+ input.type = "text";
+ input.name = type;
+ input.inputMode = "numeric";
+ input.autocomplete = "off";
+ input.required = true;
+ input.id = Helper.getUid(`CoordinateAdvancedSearch-${type}-`);
+ div.appendChild(input);
+ inputContainer.appendChild(div);
+
+ // Lie le label à l'input
+ label.setAttribute("for", input.id);
+
+ // Mask
+ const mask = document.createElement("div");
+ mask.className = "display-mask";
+ mask.textContent = "__°__'__\"";
+ // inputContainer.appendChild(mask);
+ div.appendChild(mask);
+
+ // Cardinal points
+ inputContainer.appendChild(this._createSelectCardinals(type));
+
+ return wrapper;
+ }
+
+ /**
+ * Crée la liste des points cardinaux (N/S ou O/E) pour un champ de coordonnées.
+ *
+ * @private
+ * @param {String} baseName "lon" ou "lat"
+ * @returns {HTMLSelectElement|undefined} Élément select des cardinaux ou undefined si non applicable
+ */
+ _createSelectCardinals (baseName) {
+ const options = {
+ lon : ["O", "E"],
+ lat : ["N", "S"],
+ };
+
+ if (!options[baseName]) {
+ return;
+ } else {
+ const cardinals = document.createElement("select");
+ cardinals.className = "fr-select";
+ cardinals.id = cardinals.name = Helper.getUid("CoordinateAdvancedSearch-cardinal-");
+
+ options[baseName].forEach(value => {
+ const option = document.createElement("option");
+ option.value = option.textContent = value;
+ cardinals.appendChild(option);
+ });
+
+ return cardinals;
+ }
+ }
+
+ /**
+ * @override
+ * @protected
+ * @param {CoordinateAdvancedSearchOptions} options Options d'initialisation
+ */
+ _initEvents (options) {
+ super._initEvents(options);
+
+ this.system.addEventListener("change", this._updateSystem.bind(this));
+
+ this.unit.addEventListener("change", this._updateUnits.bind(this));
+
+ this.on("change:unitType", this._updateInputsLabel.bind(this));
+
+ this.on("change:unit", this._updateInputs.bind(this));
+ }
+
+ /**
+ * Met à jour le système de référence sélectionné et réinitialise les unités disponibles.
+ *
+ * @private
+ * @param {Event} e Événement change provenant du select système
+ */
+ _updateSystem (e) {
+ const crs = this._coordinateSearchSystems[e.target.value].crs;
+ if (crs !== this._currentCoordinateSystem.crs) {
+ this._currentCoordinateSystem = this._coordinateSearchSystems[e.target.value];
+ this._currentUnit = this._coordinateSearchUnits[this._currentCoordinateSystem.type];
+ this.unit.replaceChildren();
+ for (let j = 0; j < this._currentUnit.length; j++) {
+ const unit = this._currentUnit[j];
+ const option = document.createElement("option");
+ option.value = (unit.code) ? unit.code : j;
+ option.textContent = unit.label || j;
+ this.unit.appendChild(option);
+ }
+ e.target.closest("form").dataset.unit = this.unit.value;
+ this.set("unit", this.unit.value);
+ e.target.closest("form").dataset.unitType = this._currentCoordinateSystem.type;
+ this.set("unitType", this._currentCoordinateSystem.type);
+ }
+ }
+
+ /**
+ * Met à jour l'unité sélectionnée et stocke la valeur dans le dataset du formulaire.
+ *
+ * @private
+ * @param {Event} e Événement change provenant du select unité
+ */
+ _updateUnits (e) {
+ const value = e.target.value;
+ if (this.get("unit") !== value) {
+ e.target.closest("form").dataset.unit = value;
+ this.set("unit", value);
+ }
+ }
+
+ /**
+ * Met à jour les labels des inputs en fonction du type d'unités (géographique vs métrique).
+ *
+ * @private
+ */
+ _updateInputsLabel () {
+ const unitType = this.get("unitType");
+ const degree = unitType === "Geographical";
+ const labels = {
+ "lon" : degree ? "Longitude" : "X",
+ "lat" : degree ? "Latitude" : "Y",
+ };
+ this._updateLabel(labels.lon, this.lon, true);
+ this._updateLabel(labels.lat, this.lat, true);
+
+ // Reset la valeur
+ this.lon.querySelector("input").value = "";
+ this.lat.querySelector("input").value = "";
+ }
+
+ /**
+ * Met à jour les inputs lorsque l'unité change (conversion ou activation du masquage pour DMS).
+ *
+ * @private
+ */
+ _updateInputs () {
+ const unit = this.get("unit");
+ let factor = 1;
+ if (this.get("unitType") === "Metric") {
+ factor = unit === "KM" ? 0.001 : 1000;
+ }
+ // TODO : Faire convertion ?!
+ if (unit === "DMS") {
+ this.lonLatInputs.querySelectorAll("input").forEach(input => {
+ input.value = "";
+ input.minLength = "6";
+ input.maxLength = "6";
+ input.addEventListener("beforeinput", this._boundOnLonLatBeforeInput);
+ input.addEventListener("input", this._boundOnLonLatInput);
+ });
+ } else {
+ this.lonLatInputs.querySelectorAll("input").forEach(input => {
+ if (unit === "DEC") {
+ input.value = "";
+ } else {
+ input.value = input.value === "" || isNaN(input.value) ? "" : parseFloat(input.value) * factor;
+ }
+ input.removeAttribute("minLength");
+ input.removeAttribute("maxLength");
+ input.removeEventListener("beforeinput", this._boundOnLonLatBeforeInput);
+ input.removeEventListener("input", this._boundOnLonLatInput);
+ });
+ }
+ // Réinitialise le mask de l'input (dans tous les cas)
+ this.getContent().querySelectorAll(".display-mask").forEach(mask => {
+ mask.textContent = "__°__'__\"";
+ });
+ }
+
+ /**
+ * Validation avant saisie pour les formats DMS : n'autorise que les chiffres (évite les lettres).
+ *
+ * @private
+ * @param {InputEvent} e Événement beforeinput
+ */
+ _onlonLatBeforeInput (e) {
+ // const regex = /^(?:\d{2}°\d{2}'\d{2}(?:"|''))$|^\d{6}$/;
+ const regex = /^\d+$/;
+ // Vérifie si c'est un chiffre
+ // TODO : améliorer cela ? vis à vis du insertLineBreak (touche entrée)
+ if (e.inputType.startsWith("insert") && e.inputType != "insertLineBreak" && !regex.test(e.data)) {
+ e.preventDefault();
+ }
+ }
+
+ /**
+ * Formate une chaîne de 6 caractères en DMS (DD°MM'SS").
+ *
+ * @private
+ * @param {String} value Valeur brute (6 caractères numériques)
+ * @returns {String} Chaîne formatée (ex. "12°34'56\"")
+ */
+ _format (value) {
+ const v = value.padEnd(6, "_");
+ return `${v.slice(0,2)}°${v.slice(2,4)}'${v.slice(4,6)}"`;
+ }
+
+ /**
+ * Met à jour l'affichage du masque pendant la saisie DMS.
+ *
+ * @private
+ * @param {InputEvent} e Événement input
+ */
+ _onlonLatInput (e) {
+ console.log("on lon lat inputs");
+ const value = e.target.value;
+ const mask = e.target.parentElement.querySelector(".display-mask");
+ mask.textContent = this._format(value);
+ }
+
+ /**
+ * @override
+ * @protected
+ * @param {PointerEvent} e Événement de soumission
+ */
+ _onSearch (e) {
+ super._onSearch(e);
+ // Récupère les valeurs des inputs
+ let lon = this.lon.querySelector("input").value;
+ let lat = this.lat.querySelector("input").value;
+ if (this.get("unit") === "DMS") {
+ // Transforme les DMS en degrés décimaux
+ lon = MathUtils.dmsToDecimal(
+ parseInt(lon.substring(0, 2)),
+ parseInt(lon.substring(2, 4)),
+ parseInt(lon.substring(4, 6)),
+ this.lon.querySelector("select").value
+ );
+ lat = MathUtils.dmsToDecimal(
+ parseInt(lat.substring(0, 2)),
+ parseInt(lat.substring(2, 4)),
+ parseInt(lat.substring(4, 6)),
+ this.lat.querySelector("select").value
+ );
+ } else if (this.get("unit") === "KM") {
+ // Transforme les kilomètres en mètre
+ lon = parseFloat(lon) * 1000;
+ lat = parseFloat(lat) * 1000;
+ } else {
+ // Récupère les valeurs en float
+ lon = parseFloat(lon);
+ lat = parseFloat(lat);
+ }
+
+ // Projette les coordonnées dans les coordonnées de la carte
+ let coords = [lon, lat];
+ const mapProj = this.getMap().getView().getProjection().getCode();
+ const currentProj = this._currentCoordinateSystem.crs;
+ if (mapProj !== currentProj) {
+ coords = olProjTransform(coords, currentProj, mapProj);
+ }
+
+ const geom = new Point(coords);
+ const f = new Feature({ geometry : geom });
+ // Ajout des coordonnées pour le popup
+ f.set("infoPopup", this._createInfoPopup(lon, lat));
+
+ this.dispatchEvent({
+ type : "search",
+ result : f,
+ });
+ }
+
+ /**
+ * Construit le contenu HTML à afficher dans la popup à partir des coordonnées fournies.
+ *
+ * @private
+ * @param {Number|String} lon Valeur X / longitude
+ * @param {Number|String} lat Valeur Y / latitude
+ * @returns {String} Contenu HTML (string) à injecter dans la popup
+ */
+ _createInfoPopup (lon, lat) {
+ let x, y, valueX, valueY;
+ if (this.get("unitType") === "Geographical") {
+ x = "Longitude";
+ y = "Latitude";
+
+ if (this.get("unit") === "DMS") {
+ const lon = this.lon.querySelector("input").value;
+ const lonCardinal = this.lon.querySelector("select").value;
+ valueX = `${parseInt(lon.substring(0, 2))}°${parseInt(lon.substring(2, 4))}'${parseInt(lon.substring(4, 6))}" ${lonCardinal}`;
+
+ const lat = this.lat.querySelector("input").value;
+ const latCardinal = this.lat.querySelector("select").value;
+ valueY = `${parseInt(lat.substring(0, 2))}°${parseInt(lat.substring(2, 4))}'${parseInt(lat.substring(4, 6))}" ${latCardinal}`;
+ } else {
+ valueX = `${lon} °`;
+ valueY = `${lat} °`;
+ }
+ } else {
+ x = "X";
+ y = "Y";
+ valueX = `${lon} ${this.get("unit").toLowerCase()}`;
+ valueY = `${lat} ${this.get("unit").toLowerCase()}`;
+ }
+ const infoPopup = `${x} : ${valueX}
${y} : ${valueY}`;
+ return infoPopup;
+ }
+
+
+ /**
+ * Réinitialise les champs du formulaire.
+ * @param {PointerEvent} e Événement d'effacement
+ * @protected
+ */
+ _onErase (e) {
+ super._onErase(e);
+ this.getContent().querySelectorAll(".display-mask").forEach(mask => {
+ mask.textContent = "__°__'__\"";
+ });
+ }
+
+}
+
+export default CoordinateAdvancedSearch;
+
+// Expose CoordinateAdvancedSearch as ol.control.CoordinateAdvancedSearch (for a build bundle)
+if (window.ol && window.ol.control) {
+ window.ol.control.CoordinateAdvancedSearch = CoordinateAdvancedSearch;
+}
diff --git a/src/packages/Controls/SearchEngine/InseeAdvancedSearch.js b/src/packages/Controls/SearchEngine/InseeAdvancedSearch.js
new file mode 100644
index 000000000..356d49e13
--- /dev/null
+++ b/src/packages/Controls/SearchEngine/InseeAdvancedSearch.js
@@ -0,0 +1,148 @@
+// import CSS
+import AbstractAdvancedSearch from "./AbstractAdvancedSearch";
+import Logger from "../../Utils/LoggerByDefault";
+import SearchEngineGeocodeIGN from "./SearchEngineGeocodeIGN";
+import InseeSearchService from "../../Services/InseeSearchService";
+
+var logger = Logger.getLogger("abstractAdvancedSearch");
+
+// Voir les typedefs réutilisables (types partagés) dans le dossier SearchEngine si nécessaire.
+
+/**
+ * @classdesc
+ * Contrôle de recherche avancée pour les codes INSEE.
+ *
+ * @alias ol.control.InseeAdvancedSearch
+ * @module InseeAdvancedSearch
+ * @extends AbstractAdvancedSearch
+ */
+class InseeAdvancedSearch extends AbstractAdvancedSearch {
+
+ /**
+ * @constructor
+ * @param {AbstractAdvancedSearchOptions} [options] - Options du contrôle.
+ */
+ constructor (options) {
+ super(options);
+
+ this.inseeInput.on("search", function (e) {
+ if (e.result) {
+ this.dispatchEvent(e);
+ } else {
+ // Pas de réponse, on affiche un message d'erreur
+ this.inseeInput.addMessage("Ce code INSEE est introuvable.");
+ }
+ }.bind(this));
+ }
+
+ /**
+ *
+ * @param {AbstractAdvancedSearchOptions} options Options de recherche avancée
+ * @override
+ * @protected
+ */
+ initialize (options) {
+ if (!options.name) {
+ options.name = "Code INSEE";
+ }
+ super.initialize(options);
+
+ this._inputPattern = /^(\d\d|2[A,B,a,b])\d{3}$/;
+
+ /**
+ * Nom de la classe (heritage)
+ * @private
+ */
+ this.CLASSNAME = "InseeAdvancedSearch";
+ }
+
+ /**
+ * Ajoute les inputs spécifiques au contrôle (surcouche du parent).
+ * Crée et configure l'input INSEE.
+ * @protected
+ * @returns {void}
+ */
+ addInputs () {
+ super.addInputs();
+
+ let inseeInput = this.inseeInput = new SearchEngineGeocodeIGN({
+ label : "Code INSEE",
+ hint : "Format attendu INSEE : 5 chiffres, selon le code officiel géographique (COG)",
+ searchService : new InseeSearchService({
+ autocomplete : false,
+ searchOptions : {
+ serviceOptions : {
+ fields : ["postcode"],
+ },
+ },
+ })
+ });
+
+ this.inseeInput.input.title = "Code INSEE sur 5 caractères";
+
+ // TODO : laisser validation directe par l'ordi ?
+ // this.inseeInput.input.pattern = "^(\\d\\d|2[A,B,a,b])\\d{3}$";
+ // this.inseeInput.input.maxLength = 5;
+ // this.inseeInput.input.minLength = 5;
+
+ this.inputs.push(inseeInput);
+ }
+
+ /**
+ * Initialise les événements DOM spécifiques.
+ * @protected
+ * @param {AbstractAdvancedSearchOptions} options - Options d'initialisation (transmises depuis le parent).
+ * @override
+ */
+ _initEvents (options) {
+ super._initEvents(options);
+
+ this.inseeInput.input.onkeydown = function (e) {
+ if (e.key === "Enter") {
+ this.element.requestSubmit(this.searchBtn);
+ }
+ }.bind(this);
+ }
+
+ /**
+ * Permet de supprimer les messages d'erreur, en plus du fonctionnement initial.
+ *
+ * @protected
+ * @override
+ */
+ _onErase (e) {
+ super._onErase(e);
+ this.inseeInput.removeMessages();
+ }
+
+ /**
+ * Handler de la recherche déclenchée (bouton / submit).
+ * Valide le code INSEE puis lance la recherche via le SearchEngine associé.
+ * @protected
+ * @param {PointerEvent|Event} e - Événement de recherche.
+ */
+ _onSearch (e) {
+ super._onSearch(e);
+ const insee = this.inseeInput.input.value;
+
+ if (this._inputPattern.test(insee)) {
+ this.inseeInput.removeMessages();
+ this.inseeInput.search({
+ location : insee,
+ filters : {
+ cityCode : insee,
+ }
+ });
+ } else {
+ this.inseeInput.addMessage("Le champs INSEE n'est pas valide (texte de 5 chiffres).");
+ }
+ }
+
+}
+
+export default InseeAdvancedSearch;
+
+// Expose InseeAdvancedSearch as ol.control.InseeAdvancedSearch (for a build bundle)
+if (window.ol && window.ol.control) {
+ window.ol.control.InseeAdvancedSearch = InseeAdvancedSearch;
+}
diff --git a/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js b/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js
new file mode 100644
index 000000000..d5e978089
--- /dev/null
+++ b/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js
@@ -0,0 +1,511 @@
+import Helper from "../../Utils/Helper";
+import AbstractAdvancedSearch from "./AbstractAdvancedSearch";
+import IGNSearchService from "../../Services/IGNSearchService";
+
+/**
+ * @classdesc
+ * Recherche avancée par lieu / toponyme (formulaire pour filtrer par type, code postal, INSEE).
+ *
+ * @alias ol.control.LocationAdvancedSearch
+ * @module LocationAdvancedSearch
+ */
+class LocationAdvancedSearch extends AbstractAdvancedSearch {
+
+ /**
+ * Constructeur
+ * @constructor
+ * @param {AbstractAdvancedSearchOptions} options Options du constructeur
+ * @param {String} [options.name="Lieux et toponymes"] Nom du contrôle
+ * @param {Array|Object} [options.typeList] Liste des types de lieux (catégories) ou objet clé/valeur avec tableau de sous-catégories
+ * @extends {ol.control.AbstractAdvancedSearch}
+ */
+ constructor (options) {
+ options = options || {};
+
+ options.name = options.name || "Lieux et toponymes";
+
+ // call ol.control.Control constructor
+ super(options);
+
+ // Search service
+ this.searchService = new IGNSearchService({
+ index : "poi",
+ limit : 10,
+ returnTrueGeometry : true
+ });
+
+ // Prevent popup validation
+ this.element.setAttribute("novalidate", "");
+ }
+
+ /**
+ * @override
+ * @protected
+ * @param {AbstractAdvancedSearchOptions} options Options du constructeur
+ */
+ _initEvents (options) {
+ super._initEvents(options);
+
+ this.on("expand", e => {
+ if (e.expanded) {
+ setTimeout(() => this.searchInput.focus(), 300);
+ }
+ this.searchResult.innerHTML = "";
+ });
+
+ // Do something on search (when ready)
+ setTimeout(() => {
+ this.searchService.on("search", e => this.handleSearch(e));
+ this.searchService.on("error", e => this.handleError(e));
+ });
+ }
+
+ /** Gère les erreurs de recherche.
+ * @private
+ * @param {Event} e Événement d'erreur
+ */
+ handleError (e) {
+ this.handleSearch({ nbResults : 0 });
+ }
+
+ /**
+ * Gère les résultats de la recherche.
+ * @private
+ * @param {Event} e Événement de recherche contenant les résultats
+ */
+ handleSearch (e) {
+ // Clear previous results
+ if (!e.multi) {
+ this.searchResult.innerHTML = "";
+ }
+ // Format output
+ if (e.nbResults === 1) {
+ const attr = e.attr || this.searchService.getResult(0).placeAttributes;
+ ["postcode","citycode","city","category"].forEach(field => {
+ attr[field] = attr[field] || [];
+ });
+ const info = {
+ infoPopup : "" + attr.toponym + "
" +
+ (attr.category ? ("" + (attr.category || []).join(", ") + "
") : "") +
+ (attr.postcode ? ("Code postal : " + (attr.postcode || []).join(", ") + "
") : "") ,
+ toponyme : attr.toponym,
+ postcode : attr.postcode[0],
+ postcodes : attr.postcode.join(" - "),
+ insee : attr.citycode[0],
+ citycodes : attr.citycode.join(" - "),
+ city : attr.city[0],
+ citys : attr.city.join(" - "),
+ category : attr.category[0],
+ categories : attr.category.join(" - ")
+ };
+
+ if (e.result) {
+ e.result.setProperties(info);
+ }
+ if (e.extent) {
+ e.extent.setProperties(info);
+ }
+ console.log(e);
+ this.dispatchEvent(e);
+ } else {
+ this.element.parentElement.parentElement.scrollTop = 0;
+ if (e.nbResults === 0) {
+ this.searchResult.innerHTML = "";
+ const li = document.createElement("li");
+ li.className = "fr-message--error";
+ li.innerText = "Aucun résultat";
+ if (!this.searchInput.value) {
+ li.innerText += " : précisez votre recherche en renseignant le lieu.";
+ }
+ this.searchResult.appendChild(li);
+ li.addEventListener("click", () => {
+ li.remove();
+ });
+ } else {
+ this.handleMultipleResults(e);
+ }
+ }
+ }
+
+ /**
+ * @override
+ * @protected
+ * @param {AbstractAdvancedSearchOptions} options Options du constructeur
+ */
+ initialize (options) {
+ super.initialize(options);
+ /**
+ * Nom de la classe (heritage)
+ * @private
+ */
+ this.CLASSNAME = "LocationAdvancedSearch";
+
+ // Get type list from capabilities if not provided
+ fetch("https://data.geopf.fr/geocodage/getCapabilities").then(response => {
+ return response.json();
+ }).then (json => {
+ // Get list from capabilities
+ const values = json.indexes.find(i => i.id==="poi").fields.find(f => f.name==="category").values;
+ this._typeList = values;
+ // Set categories if not provided in options
+ if (!options.typeList) {
+ this.setCategories(values);
+ }
+ }).catch(() => {
+ console.log("error");
+ });
+ this._typeList = {};
+ // Default values
+ const typeList = [ "administratif", "aérodrome", "cimetière", "construction ponctuelle", "construction surfacique", "cours d'eau" ];
+ this.set("typeList", options.typeList || typeList);
+ }
+
+ /** Set categories list
+ * @param {Array|Object} categories Liste des catégories
+ * @param {HTMLSelectElement} [select] Élément select à remplir (par défaut celui du formulaire)
+ */
+ setCategories (categories, select) {
+ select = select || this.element.querySelector("select[name='category']");
+ select.innerHTML = "";
+ let typeList = [];
+ const isArray = Array.isArray(categories);
+ if (isArray) {
+ typeList = categories;
+ } else if (typeof categories === "object") {
+ typeList = Object.keys(categories);
+ } else {
+ return;
+ }
+ typeList.forEach(k => {
+ const typeOption = document.createElement("option");
+ typeOption.value = k.toLowerCase();
+ typeOption.innerText = k;
+ select.appendChild(typeOption);
+ if (!isArray && Array.isArray(categories[k])) {
+ typeOption.className = "option";
+ categories[k].forEach(v => {
+ const subOption = document.createElement("option");
+ subOption.value = v.toLowerCase();
+ subOption.className = "subOption";
+ subOption.innerHTML = " " + v;
+ select.appendChild(subOption);
+ });
+ }
+ });
+ if (this.filter) {
+ // Set current value
+ select.value = this.filter.category;
+ // If none selected, reset to first
+ if (!select.value) {
+ select.selectedIndex = 0;
+ this.filter.category = select.value;
+ }
+ }
+ }
+
+ /**
+ * Affiche la liste des résultats multiples et permet la sélection.
+ * @private
+ * @param {Event} e Événement de recherche contenant les résultats
+ */
+ handleMultipleResults (e) {
+ const results = this.searchService.getResult();
+ results.forEach((result, i) => {
+ // console.log(result);
+ const attr = result.placeAttributes;
+ const li = document.createElement("li");
+ li.className = "search-result-item" + (i>=5 ? " hidden" : "");
+ this.searchResult.appendChild(li);
+ // link for accessibility
+ const a = document.createElement("a");
+ li.appendChild(a);
+ a.className = "fr-icon-map-pin-2-line";
+ a.href = "#";
+ // Format title
+ const postCode = (attr.postcode && attr.postcode[0]) ? `, ${attr.postcode[0]}` : "";
+ const text = (attr.toponym || "") + postCode + " (" + (attr.category || []).join(", ") + ") - " + (attr.city || []).join(", ");
+ a.title = a.innerText = text;
+ // on click on li or link
+ li.addEventListener("click", () => a.click());
+ a.addEventListener("click", e => {
+ e.preventDefault();
+ const features = this.searchService.getResultFeatures(i);
+ const event = {
+ type : "search",
+ multi : true,
+ attr : attr,
+ extent : features.extent,
+ result : features.feature,
+ nbResults : 1
+ };
+ this.searchService.dispatchEvent(event);
+ });
+ });
+ // Actions
+ const li = document.createElement("li");
+ this.searchResult.appendChild(li);
+ // more options
+ if (results.length > 5) {
+ const plusBtn = document.createElement("button");
+ plusBtn.className = "fr-btn fr-btn--sm fr-btn--tertiary";
+ plusBtn.innerText = "Afficher plus de résultats";
+ plusBtn.addEventListener("click", () => {
+ plusBtn.remove();
+ this.searchResult.querySelectorAll(".hidden").forEach(el => {
+ el.classList.remove("hidden");
+ });
+ });
+ li.appendChild(plusBtn);
+ }
+ // clear button
+ li.className = "search-result-actions";
+ const okBtn = document.createElement("button");
+ okBtn.className = "fr-btn fr-btn--sm fr-btn--tertiary";
+ okBtn.innerText = "Effacer";
+ okBtn.addEventListener("click", () => {
+ this.searchResult.innerHTML = "";
+ this.element.parentElement.parentElement.scrollTop = 0;
+ });
+ li.appendChild(okBtn);
+ }
+
+ /**
+ * Crée un conteneur label + input pour le formulaire.
+ * @private
+ * @param {String} text Texte du label
+ * @param {String} type Classe CSS du conteneur
+ * @param {HTMLElement} input Élément input à rattacher
+ * @param {String} [hint] Texte d'aide optionnel
+ * @returns {HTMLElement} Conteneur HTML
+ */
+ _getLabelContainer (text, type, input, hint = "") {
+ const container = document.createElement("div");
+ container.className = type;
+ this.inputs.push(container);
+ const label = document.createElement("label");
+ label.className = "fr-label";
+ let labelText = text;
+ if (hint) {
+ labelText = `${text} ${hint}`;
+ }
+ label.innerHTML = labelText;
+ container.appendChild(label);
+ if (input) {
+ label.setAttribute("for", input.id);
+ container.appendChild(input);
+ }
+ // Error message
+ const errorDiv = document.createElement("div");
+ errorDiv.className = "GPMessagesGroup fr-messages-group";
+ container.appendChild(errorDiv);
+
+ // container
+ return container;
+ }
+
+ /**
+ * @override
+ * @protected
+ */
+ addInputs () {
+ super.addInputs();
+
+ this.searchResult = document.createElement("ul");
+ this.searchResult.className = "search-result";
+ this.inputs.push(this.searchResult);
+
+ // Legend
+ const legend = document.createElement("legend");
+ legend.className = "fr-fieldset__legend fr-fieldset__legend--regular";
+ legend.id = Helper.getUid("LocationAdvancedSearch-legend-");
+ const hint = document.createElement("span");
+ hint.className = "fr-hint-text";
+ hint.textContent = "* Champs obligatoires";
+ legend.appendChild(hint);
+ this.inputs.push(legend);
+
+ // Type
+ const typeSelect = document.createElement("select");
+ typeSelect.className = "fr-select";
+ typeSelect.id = Helper.getUid("LocationAdvancedSearch-type-");
+ typeSelect.name = "category";
+ this._getLabelContainer("Type", "fr-select-group", typeSelect);
+ this.setCategories(this.get("typeList"), typeSelect);
+ typeSelect.addEventListener("change", () => {
+ this.filter.category = typeSelect.value;
+ });
+
+ // Search input
+ const searchInput = this.searchInput = document.createElement("input");
+ searchInput.className = "fr-input";
+ searchInput.type = "text";
+ searchInput.name = "search";
+ searchInput.setAttribute("minlength", "3");
+ searchInput.required = true;
+ searchInput.id = Helper.getUid("LocationAdvancedSearch-search-");
+ this._getLabelContainer("Renseigner un lieu *", "fr-input-group", searchInput, null);
+
+ // Code postal
+ const postalInput = document.createElement("input");
+ postalInput.className = "fr-input";
+ postalInput.type = "text";
+ postalInput.name = "postalCode";
+ postalInput.pattern = "^(\\d{5})$";
+ postalInput.title = "Code postal à 5 chiffres";
+ postalInput.id = Helper.getUid("LocationAdvancedSearch-postal-");
+ this._getLabelContainer("Code postal", "fr-input-group", postalInput);
+ postalInput.addEventListener("change", () => {
+ this.filter.postcode = postalInput.value;
+ });
+
+ // Code INSEE
+ const inseeInput = document.createElement("input");
+ inseeInput.className = "fr-input";
+ inseeInput.name = "cityCode";
+ inseeInput.type = "text";
+ inseeInput.pattern = "^(\\d\\d|2[A,B,a,b])\\d{3}$";
+ inseeInput.title = "Code INSEE sur 5 caractères";
+ inseeInput.id = Helper.getUid("LocationAdvancedSearch-insee-");
+ this._getLabelContainer("Code INSEE", "fr-input-group", inseeInput, "Format attendu INSEE : 5 chiffres, selon le code officiel géographique (COG)");
+ inseeInput.addEventListener("change", () => {
+ this.filter.citycode = inseeInput.value.toUpperCase();
+ });
+
+ this.filter = {
+ category : typeSelect.value,
+ postcode : postalInput.value.toUpperCase(),
+ citycode : inseeInput.value
+ };
+ }
+
+ /**
+ * @protected
+ * @override
+ * @param {PointerEvent} e Événement d'effacement
+ */
+ _onErase (e) {
+ super._onErase(e);
+ this.element.querySelectorAll("select").forEach(input => {
+ input.selectedIndex = 0;
+ });
+ this.element.querySelectorAll("input").forEach(input => {
+ input.value = "";
+ });
+ this.searchResult.innerHTML = "";
+ this._clearMessages();
+ this.filter = {
+ category : this.element.querySelector("select[name='category']").value,
+ postcode : "",
+ citycode : ""
+ };
+ }
+
+ /** Clear error messages in the form
+ * @private
+ */
+ _clearMessages () {
+ this.element.querySelectorAll(".fr-message--error").forEach(msg => msg.remove());
+ }
+
+ /** Show error message for a given input
+ * @param {String} name Input name
+ * @param {String} [message] Message to show (if none remove message)
+ * @private
+ */
+ _showMessage (name, message) {
+ const div = this.element.querySelector("input[name='" + name + "']").nextSibling;
+ if (message) {
+ const msg = document.createElement("p");
+ msg.className = "fr-message fr-message--error";
+ msg.textContent = message;
+ div.appendChild(msg);
+ } else {
+ div.innerHTML = "";
+ }
+ }
+
+ /**
+ * @protected
+ * @override
+ * @param {PointerEvent} e Événement de soumission
+ * param {String} [commune] Nom de la commune (optionnel)
+ */
+ _onSearch (e, commune) {
+ super._onSearch(e);
+ const value = commune || this.searchInput.value;
+ // Check values
+ this._clearMessages();
+ /*
+ if (value.length < 3) {
+ this._showMessage("search", "Veuillez saisir au moins 3 caractères pour lancer la recherche.");
+ return;
+ }
+ */
+ if (this.filter.postcode) {
+ if (!this.element.querySelector("input[name='postalCode']").checkValidity()) {
+ this._showMessage("postalCode", "Le code postal doit être composé de 5 chiffres.");
+ return;
+ }
+ }
+ if (this.filter.citycode) {
+ if (!this.element.querySelector("input[name='cityCode']").checkValidity()) {
+ this._showMessage("cityCode", "Le code INSEE doit être composé de 5 caractères (chiffres ou 2A, 2B).");
+ return;
+ }
+ }
+ // Get value from code if needed
+ if (!value && this.filter.citycode) {
+ // Search commune name as search string
+ fetch(`https://geo.api.gouv.fr/communes?code=${this.filter.citycode}&format=json&fields=nom`).then(response => {
+ return response.json();
+ }).then (json => {
+ if (json && json.length) {
+ const commune = json[0].nom;
+ if (commune) {
+ this._onSearch(e, commune);
+ } else {
+ this.handleSearch({ nbResults : 0 });
+ }
+ }
+ }).catch(() => {
+ this.handleSearch({ nbResults : 0 });
+ });
+ return;
+ } else if (!value && this.filter.postcode) {
+ // Search commune name as search string
+ fetch(`https://apicarto.ign.fr/api/codes-postaux/communes/${this.filter.postcode}`).then(response => {
+ return response.json();
+ }).then (json => {
+ if (json && json.length) {
+ const commune = json[0].nomCommune;
+ if (commune) {
+ this._onSearch(e, commune);
+ } else {
+ this.handleSearch({ nbResults : 0 });
+ }
+ }
+ }).catch(() => {
+ this._showMessage("postalCode", "Aucune commune trouvée pour ce code postal.");
+ this.handleSearch({ nbResults : 0 });
+ });
+ return;
+ } else if (value.length < 3) {
+ this._showMessage("search", "Veuillez saisir au moins 3 caractères pour lancer la recherche.");
+ return;
+ }
+
+ // Search
+ this.searchService.search({
+ location : value,
+ filters : this.filter
+ });
+ }
+
+}
+
+export default LocationAdvancedSearch;
+
+// Expose LocationAdvancedSearch as ol.control.LocationAdvancedSearch (for a build bundle)
+if (window.ol && window.ol.control) {
+ window.ol.control.LocationAdvancedSearch = LocationAdvancedSearch;
+}
diff --git a/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js b/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js
new file mode 100644
index 000000000..28adc14aa
--- /dev/null
+++ b/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js
@@ -0,0 +1,867 @@
+import def from "ajv/dist/vocabularies/discriminator";
+import InseeSearchService from "../../Services/InseeSearchService";
+import Helper from "../../Utils/Helper";
+import AbstractAdvancedSearch from "./AbstractAdvancedSearch";
+import IGNSearchService from "../../Services/IGNSearchService";
+
+/**
+ * @classdesc
+ * Contrôle de recherche parcellaire.
+ *
+ * @extends {AbstractAdvancedSearch}
+ * @alias ol.control.ParcelAdvancedSearch
+ * @module ParcelAdvancedSearch
+ */
+class ParcelAdvancedSearch extends AbstractAdvancedSearch {
+
+ /**
+ * Constructeur du contrôle de recherche avancée.
+ * @param {AbstractAdvancedSearchOptions} options Options du constructeur
+ * @param {String} [options.name="Parcelles"] Nom du contrôle
+ * @extends {ol.control.AbstractAdvancedSearch}
+ */
+ constructor (options) {
+ options = options || {};
+
+ options.name = options.name || "Parcelles cadastrales";
+
+ // call ol.control.Control constructor
+ super(options);
+
+ // prevent HTML5 validation
+ this.element.setAttribute("novalidate", "");
+
+ this.searchService = new IGNSearchService({
+ index : "parcel",
+ limit : 1,
+ returnTrueGeometry : true
+ });
+ this.searchService.on("search", e => this.handleSearch(e));
+ this.searchService.on("error", e => this.handleError(e));
+ }
+
+ /** An error occured during search
+ * @param {Object} e Event
+ */
+ handleError (e) {
+ console.log("error", e);
+ this.dispatchEvent(e);
+ }
+
+ /** Search completed
+ * @param {Object} e Event
+ */
+ handleSearch (e) {
+ if (e.nbResults === 0) {
+ this._showMessage("numero", "Aucune parcelle ne correspond à ce numéro.");
+ return;
+ }
+ // Format result
+ const attr = e.attr || this.searchService.getResult(0).placeAttributes;
+ delete attr.truegeometry;
+ attr.infoPopup = `${attr.id}
+ Parcelle n° ${attr.number.replace(/^0{1,4}/g,"")}
+ Feuille ${attr.oldmunicipalitycode}-${attr.section}
+ ${attr.city}
+ `;
+ // Set properties
+ e.result?.setProperties(attr);
+ e.extent?.setProperties(attr);
+
+ this.dispatchEvent(e);
+ }
+
+
+ /** Clear error messages in the form
+ * @private
+ */
+ _clearMessages () {
+ this.element.querySelectorAll(".fr-message--error").forEach(msg => msg.remove());
+ }
+
+ /** Show error message for a given input
+ * @param {String} name Input name
+ * @param {String} [message] Message to show (if none remove message)
+ * @param {String} [what="error"] Message type (error, warning, info)
+ * @private
+ */
+ _showMessage (name, message, what) {
+ const div = this.element.querySelector("[name='" + name + "']").parentElement.querySelector(".GPMessagesGroup");
+ if (message) {
+ const msg = document.createElement("p");
+ msg.className = "fr-message fr-message--" + (what || "error");
+ msg.textContent = message;
+ div.innerHTML = "";
+ div.appendChild(msg);
+ } else {
+ div.innerHTML = "";
+ }
+ }
+
+ /** Change the section
+ * @private
+ */
+ setSection () {
+ const prefix = this.prefixInput.value;
+ const section = this.sectionInput.value;
+ this.numberList.innerHTML = "";
+ this._showMessage("section", "chargement en cours", "info");
+ this._fetchCadastre(this.communeId, this.arrondId, prefix, section).then(data => {
+ this._showMessage("section", "");
+ const section = this.sectionInput.value;
+ if (data && data.features && data.features[0].properties.section === section) {
+ this.numberInput.focus();
+ const numbers = [];
+ data.features.forEach(numero => numbers.push(numero.properties.numero));
+ // Sort numbers
+ numbers.sort().forEach(numero => {
+ const option = document.createElement("li");
+ option.value = option.textContent = numero.replace(/^0{1,4}/g,"");
+ option.id = Helper.getUid("GPautoCompleteOption-");
+ option.addEventListener("click", () => {
+ this.numberInput.value = option.value;
+ this._onSearch();
+ this.numberInput.blur();
+ this.numberInput.ariaExpanded = "false";
+ this.numberInput.setAttribute("aria-activedescendant", "");
+ });
+ this.numberList.appendChild(option);
+ });
+ this.filterListNumber();
+ }
+ });
+ }
+
+ /**
+ * Show sections for a given prefix
+ * @param {String} prefix Prefix code
+ */
+ setFeuille (prefix) {
+ const feuilles = this.feuilles[prefix];
+ this.sectionInput.innerHTML = "";
+ if (feuilles) {
+ const section = document.createElement("option");
+ section.value = "";
+ section.textContent = "Sélectionner une section";
+ this.sectionInput.appendChild(section);
+ let previous = "";
+ this.feuilles[prefix].sort().forEach(key => {
+ if (key !== previous) {
+ const section = document.createElement("option");
+ section.value = section.textContent = key;
+ this.sectionInput.appendChild(section);
+ previous = key;
+ }
+ });
+ this.sectionInput.removeAttribute("disabled");
+ this.sectionInput.focus();
+ } else {
+ this.sectionInput.setAttribute("disabled", "disabled");
+ }
+ this.sectionInput.dispatchEvent(new Event("change"));
+ }
+
+ /** Set the commune
+ * @param {String} [id] Commune INSEE code
+ * @param {String} [arrond] Arrondissement code
+ */
+ setCommune (id="", arrond="") {
+ if (this.communeId !== id) {
+ const prefixInput = this.prefixInput;
+
+ this.communeId = id;
+ this.arrondId = arrond;
+ prefixInput.innerHTML = "";
+ if (id) {
+ this._showMessage("commCode", "chargement en cours", "info");
+ // Fetch prefixes and sections for the selected commune
+ this._fetchCadastre(id, arrond).then(data => {
+ this._showMessage("commCode", "");
+ this.feuilles = {};
+
+ data.features.forEach(feuille => {
+ if (!this.feuilles[feuille.properties.com_abs]) {
+ this.feuilles[feuille.properties.com_abs] = [];
+ }
+ this.feuilles[feuille.properties.com_abs].push(feuille.properties.section);
+ });
+ let previous = "";
+ Object.keys(this.feuilles).sort().forEach(key => {
+ if (key !== previous) {
+ const prefix = document.createElement("option");
+ prefix.value = prefix.textContent = key;
+ prefixInput.appendChild(prefix);
+ previous = key;
+ }
+ });
+ this.setFeuille(prefixInput.value);
+ });
+ prefixInput.removeAttribute("disabled");
+ } else {
+ prefixInput.setAttribute("disabled", "disabled");
+ this.setFeuille();
+ }
+ }
+ };
+
+ /**
+ * @override
+ * @protected
+ * @param {AbstractAdvancedSearchOptions} options Options du constructeur
+ */
+ _initEvents (options) {
+ super._initEvents(options);
+
+ // Focus commune input on expand
+ this.on("expand", e => {
+ if (e.expanded) {
+ setTimeout(() => comCodeInput.focus(), 300);
+ }
+ });
+
+ // Inputs
+ const comCodeInput = this.comCodeInput;
+ const autocompleteList = this.autocompleteList;
+ const prefixInput = this.prefixInput;
+ const sectionInput = this.sectionInput;
+
+ // Current selected commune
+ let communeName = "";
+ this.communeId = "";
+ // Autocomplete selected index
+ let selectedIndex = -1;
+ let parcelIndex = -1;
+
+ // Show/hide autocomplete list
+ const showAutocomplete = (b) => {
+ if (communeName !== comCodeInput.value) {
+ this.setCommune();
+ }
+ if (b !== false) {
+ // Show autocomplete only if input value matches selected commune
+ b = (communeName === comCodeInput.value);
+ }
+ if (b) {
+ comCodeInput.setAttribute("aria-expanded", "true");
+ comCodeInput.setAttribute("aria-activedescendant", autocompleteList.children[selectedIndex]?.id || "");
+ } else {
+ comCodeInput.setAttribute("aria-expanded", "false");
+ autocompleteList.children[selectedIndex]?.classList.remove("active");
+ comCodeInput.setAttribute("aria-activedescendant", autocompleteList.children[selectedIndex]?.id || "");
+ selectedIndex = -1;
+ }
+ };
+
+
+ // Keyboard navigation on autocomplete list
+ let previousValue = "";
+ comCodeInput.addEventListener("keydown", e => {
+ // Prevent default behavior for navigation keys if elements are disabled
+ if (e.key === "Tab") {
+ if (!e.shiftKey // backward tab
+ && comCodeInput.value // input not empty
+ && previousValue !== comCodeInput.value // value not changed
+ && prefixInput.getAttribute("disabled") === "disabled" // prefix disabled
+ ) {
+ e.preventDefault();
+ comCodeInput.dispatchEvent(new Event("change"));
+ }
+ }
+ previousValue = comCodeInput.value;
+ // Autocomplete navigation
+ const autoOptions = autocompleteList.children;
+ if (autoOptions.length) {
+ let index = selectedIndex;
+ switch (e.key) {
+ case "Backspace": {
+ index = -1;
+ if (comCodeInput.value.length > 5) {
+ comCodeInput.value = comCodeInput.value.slice(0,6);
+ }
+ break;
+ }
+ case "Escape": {
+ index = -1;
+ break;
+ }
+ case "ArrowDown": {
+ index++;
+ if (index >= autoOptions.length) {
+ index = -1;
+ }
+ e.preventDefault();
+ break;
+ }
+ case "ArrowUp": {
+ index--;
+ if (index < -1) {
+ index += autoOptions.length +1;
+ }
+ e.preventDefault();
+ break;
+ }
+ case "Enter": {
+ if (index >= 0 && index < autoOptions.length) {
+ autoOptions[selectedIndex]?.classList.remove("active");
+ autoOptions[index].click();
+ index = -1;
+ }
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+ if (selectedIndex !== index) {
+ // Update selected option
+ autoOptions[selectedIndex]?.classList.remove("active");
+ selectedIndex = index;
+ autoOptions[selectedIndex]?.classList.add("active");
+ showAutocomplete(["ArrowUp", "ArrowDown"].includes(e.key));
+ // Scroll to selected option
+ autoOptions[selectedIndex]?.scrollIntoView({ block : "nearest" });
+ }
+ }
+ });
+
+ // Show autocomplete on input
+ comCodeInput.addEventListener("keyup", e => {
+ if (["ArrowUp", "ArrowDown", "Enter", "Escape"].includes(e.key)) {
+ e.preventDefault();
+ return;
+ }
+ showAutocomplete(e);
+ });
+ comCodeInput.addEventListener("focus", showAutocomplete);
+ comCodeInput.addEventListener("blur", (e) => {
+ if (e.relatedTarget !== autocompleteList) {
+ showAutocomplete(false);
+ }
+ });
+
+ // Fomat commune name for display / postal codes
+ function formatCommune (commune, cpost) {
+ const name = `${commune.arrond || commune.code} ${commune.nom}`;
+ if (cpost && commune.codesPostaux) {
+ return `${name}, ${commune.codesPostaux.join(", ")} (postal)`;
+ }
+ return name;
+ }
+
+ // Fetch commune data on change
+ comCodeInput.addEventListener("change", () => {
+ //check valid input
+ if (!comCodeInput.checkValidity()) {
+ this._showMessage("commCode", "Le code INSEE doit être composé de 5 caractères (chiffres ou 2A, 2B) ou le code postal de 5 chiffres.");
+ return;
+ } else {
+ this._clearMessages();
+ }
+ this._showMessage("commCode", "chargement en cours", "info");
+ this._fetchCommuneData(comCodeInput.value).then(data => {
+ this._showMessage("commCode", "");
+ // Clear previous suggestions
+ autocompleteList.innerHTML = "";
+ communeName = "";
+ if (data.length === 0) {
+ // errror message
+ this._showMessage("commCode", "Aucune commune ne correspond à ce code INSEE ou code postal.");
+ this.setCommune();
+ } else if (data.length === 1) {
+ communeName = comCodeInput.value = formatCommune(data[0]);
+ this.setCommune(data[0].code, data[0].arrond);
+ } else {
+ data.forEach(commune => {
+ const type = commune.codesPostaux ? "code INSEE" : "code postal";
+ const option = document.createElement("li");
+ option.className = "GPautoCompleteOption";
+ option.setAttribute("role", "option");
+ option.id = Helper.getUid("GPautoCompleteOption-");
+ option.title = option.textContent = formatCommune(commune, type);
+ option.addEventListener("click", () => {
+ communeName = comCodeInput.value = formatCommune(commune);
+ this.setCommune(commune.code, commune.arrond);
+ showAutocomplete(false);
+ });
+ autocompleteList.appendChild(option);
+ });
+ communeName = comCodeInput.value;
+ this.setCommune();
+ comCodeInput.setAttribute("aria-expanded", "true");
+ }
+ });
+ });
+
+ // Fetch sections
+ prefixInput.addEventListener("change", () => {
+ this.setFeuille(prefixInput.value);
+ });
+
+ // Fetch parcelles number
+ let selectTout = null;
+ let hascliked = false;
+ // Handle click and change separately to manage click on the list
+ sectionInput.addEventListener("click", () => {
+ hascliked = true;
+ });
+ sectionInput.addEventListener("change", e => {
+ // Wait for click event to be handled first
+ setTimeout(() => {
+ clearTimeout(selectTout);
+ // If clicked, do it immediately, else wait a bit (use key to navigate the list)
+ selectTout = setTimeout(() => {
+ if (sectionInput.value) {
+ this.numberInput.removeAttribute("disabled");
+ this.setSection();
+ } else {
+ this.numberInput.setAttribute("disabled", "disabled");
+ }
+ }, hascliked ? 0 : 500);
+ });
+ hascliked = false;
+ });
+
+ // Handle listbox for number input
+ this.numberInput.addEventListener("focus", () => {
+ this.numberInput.ariaExpanded = "true";
+ });
+ this.numberInput.addEventListener("blur", (e) => {
+ if (e.relatedTarget !== this.numberList) {
+ this.numberInput.ariaExpanded = "false";
+ }
+ });
+
+ // Filter number list on keyup
+ this.numberInput.addEventListener("keydown", (e) => {
+ if (["ArrowUp", "ArrowDown", "Enter", "Escape"].includes(e.key)) {
+ e.preventDefault();
+ return;
+ }
+ });
+ this.numberInput.addEventListener("keyup", (e) => {
+ this.filterListNumber();
+ let index = parcelIndex;
+ switch (e.key) {
+ case "ArrowDown": {
+ // Next non hidden option
+ for (let i=parcelIndex +1; i < this.numberList.children.length; i++) {
+ if (this.numberList.children[i].style.display !== "none") {
+ index = i;
+ break;
+ }
+ }
+ if (index >= this.numberList.children.length) {
+ index = -1;
+ }
+ e.preventDefault();
+ break;
+ }
+ case "ArrowUp": {
+ // Previous non hidden option
+ for (let i=parcelIndex -1; i >= -1; i--) {
+ if (i === -1 || this.numberList.children[i].style.display !== "none") {
+ index = i;
+ break;
+ }
+ }
+ if (index < -1) {
+ index += this.numberList.children.length +1;
+ }
+ e.preventDefault();
+ break;
+ }
+ case "Escape": {
+ index = -1;
+ this.numberInput.ariaExpanded = "false";
+ break;
+ }
+ case "Enter": {
+ //this.numberList.children[index]?.click();
+ const value = this.numberList.children[index]?.value || "";
+ if (value) {
+ this.numberInput.value = value;
+ this._onSearch();
+ this.filterListNumber();
+ index = -1;
+ setTimeout(() => this.numberInput.ariaExpanded = "false");
+ }
+ break;
+ }
+ default: {
+ // reset selection
+ index = -1;
+ break;
+ }
+ }
+ if (parcelIndex !== index) {
+ // Update selected option
+ this.numberList.children[parcelIndex]?.classList.remove("active");
+ parcelIndex = index;
+ this.numberList.children[parcelIndex]?.classList.add("active");
+ this.numberInput.setAttribute("aria-activedescendant", this.numberList.children[parcelIndex]?.id || "");
+ // Scroll to selected option
+ this.numberInput.ariaExpanded = "true";
+ this.numberInput.setAttribute("aria-activedescendant", this.numberList.children[parcelIndex]?.id || "");
+ this.numberList.children[parcelIndex]?.scrollIntoView({ block : "nearest" });
+ }
+ });
+ }
+
+
+ /** Filter listbox options on input value
+ */
+ filterListNumber () {
+ const filter = this.numberInput.value.toUpperCase();
+ if (this.currentFilter === filter) {
+ return;
+ }
+ this.currentFilter = filter;
+ const options = this.numberList.querySelectorAll("li");
+ let hasNumber = false;
+ options.forEach(option => {
+ if (option.textContent.toUpperCase().indexOf(filter) > -1) {
+ option.style.display = "";
+ hasNumber = true;
+ } else {
+ option.style.display = "none";
+ }
+ });
+ if (!hasNumber) {
+ this._showMessage("numero", "Aucun numéro de parcelle ne correspond à cette saisie.");
+ } else {
+ this._showMessage("numero", "");
+ this.numberInput.ariaExpanded = "true";
+ }
+ }
+
+ /**
+ * @override
+ * @protected
+ * @param {AbstractAdvancedSearchOptions} options Options du constructeur
+ */
+ initialize (options) {
+ super.initialize(options);
+ /**
+ * Nom de la classe (heritage)
+ * @private
+ */
+ this.CLASSNAME = "ParcelAdvancedSearch";
+
+ this.insseSearchService = new InseeSearchService();
+ }
+
+ /**
+ * Crée un conteneur label + input pour le formulaire.
+ * @private
+ * @param {String} text Texte du label
+ * @param {String} type Classe CSS du conteneur
+ * @param {HTMLElement} input Élément input à rattacher
+ * @param {String} [hint] Texte d'aide optionnel
+ * @returns {HTMLElement} Conteneur HTML
+ */
+ _getLabelContainer (text, type, input, hint = "") {
+ const container = document.createElement("div");
+ container.className = type;
+ this.inputs.push(container);
+ const label = document.createElement("label");
+ label.className = "fr-label";
+ let labelText = text;
+ if (hint) {
+ labelText = `${text} ${hint}`;
+ }
+ label.innerHTML = labelText;
+ container.appendChild(label);
+ if (input) {
+ label.setAttribute("for", input.id);
+ container.appendChild(input);
+ }
+ // Error message
+ const errorDiv = document.createElement("div");
+ errorDiv.className = "GPMessagesGroup fr-messages-group";
+ container.appendChild(errorDiv);
+
+ // container
+ return container;
+ }
+
+ /**
+ * Récupère les données d'une commune via l'API geo.api.gouv.fr
+ * @private
+ * @param {String} code Code INSEE ou code postal
+ * @returns {Promise} Promesse avec les données de la commune : nom + code + codesPostaux (si codepostal))
+ */
+ async _fetchCommuneData (code) {
+ const baseURL = "https://geo.api.gouv.fr/communes";
+ const url1 = `${baseURL}?code=${code}&format=json&fields=nom,code`;
+ const url2 = `${baseURL}?codePostal=${code}&format=json&fields=nom,codesPostaux`;
+ const param ={
+ headers : {
+ "Content-Type" : "application/json",
+ },
+ };
+ // Fetch both insee code and postal code
+ const fetchtable = [
+ fetch(url1, param),
+ fetch(url2, param),
+ ];
+ // For municipal arrondissements, fetch them too
+ if (/^75|^6900|^6938|^1300|^1301|^1320|^1321/.test(code)) {
+ fetchtable.push(
+ fetch(url1 + "&type=arrondissement-municipal", param),
+ );
+ fetchtable.push(
+ fetch(url2 + "&type=arrondissement-municipal", param),
+ );
+ }
+ // Process responses
+ return Promise.all(fetchtable).then(responses => {
+ return Promise.all(responses.map(res => res.json())).then(json => {
+ // Handle insee code
+ if (json[2] && json[2].length) {
+ json[2].forEach(com => {
+ com.arrond = com.code;
+ switch (com.code.slice(0,2)) {
+ // Paris
+ case "75": {
+ com.code = "75056";
+ break;
+ }
+ // Lyon
+ case "69": {
+ com.code = "69123";
+ break;
+ }
+ // Marseille
+ case "13": {
+ com.code = "13055";
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+ });
+ }
+ // Handle arrondissements municipaux
+ if (json[3] && json[3].length) {
+ const arrond = json[3][0];
+ const cpost = arrond.codesPostaux[0];
+ if (json[1].length) {
+ // Filter out duplicates with same postal code
+ const filter = json[1].filter(com => com.codesPostaux.includes(cpost) >= 0);
+ json[1] = json[1].filter(com => com.codesPostaux.includes(cpost) < 0);
+ const com = filter.find(com => com.code !== arrond.code);
+ if (com) {
+ arrond.arrond = arrond.code;
+ arrond.code = com.code;
+ }
+ }
+ }
+ return json[0].concat(json[1]).concat(json[2]||[]).concat(json[3]||[]);
+ });
+ }).catch(error => {
+ this._showMessage("commCode", "Une erreur est survenue lors de la récupération des données de la commune.");
+ return [];
+ });
+ }
+
+ /**
+ * Récupère les feuilles cadastrales d'une commune via le WFS Geopf
+ * @private
+ * @param {String} code Code INSEE de la commune
+ * @param {String} [arrond] Code de l'arrondissement (pour les communes avec arrondissements municipaux)
+ * @param {String} [prefix] Préfixe de la parcelle
+ * @param {String} [section] Section de la parcelle
+ * @returns {Promise} Promesse avec les données GeoJSON (feuilles ou parcelles si section renseignée)
+ */
+ async _fetchCadastre (code, arrond, prefix, section) {
+ const domtom = ["97","98"].includes(code.slice(0,2));
+ const dep = code.slice(0, domtom ? 3 : 2);
+ const com = code.slice(domtom ? 3 : 2, 5);
+ const url = "https://data.geopf.fr/wfs/ows?";
+ const params = {
+ service : "WFS",
+ version : "2.0.0",
+ request : "GetFeature",
+ typename : "CADASTRALPARCELS.PARCELLAIRE_EXPRESS:" + (section ? "parcelle" : "feuille"),
+ outputFormat : "application/json",
+ srsName : "CRS:84",
+ count : "1000",
+ propertyName : section ? "com_abs,section,numero" : "com_abs,section,code_arr",
+ cql_filter : `code_dep='${dep}' and code_com='${com}'` + (arrond ? `and code_arr='${arrond.slice(2)}'` : "") + (section ? ` and com_abs='${prefix}' and section='${section}'` : "")
+ };
+ const queryString = new URLSearchParams(params).toString();
+ const fullUrl = url + queryString;
+ // Abort previous request
+ if (this.controller) {
+ this.controller.abort();
+ }
+ // New request
+ this.controller = new AbortController();
+ const response = await fetch(fullUrl, {
+ headers : {
+ "Content-Type" : "application/json",
+ },
+ signal : this.controller.signal
+ }).catch(error => {
+ return null;
+ });
+ this.controller = null;
+ if (!response) {
+ return {};
+ }
+ const data = await response.json();
+ return data;
+ }
+
+ /** Add inputs on form
+ * @override
+ * @protected
+ */
+ addInputs () {
+ super.addInputs();
+
+ // Legend
+ const legend = document.createElement("legend");
+ legend.className = "fr-fieldset__legend fr-fieldset__legend--regular";
+ legend.id = Helper.getUid("ParcelAdvancedSearch-legend-");
+ const hint = document.createElement("span");
+ hint.className = "fr-hint-text";
+ hint.textContent = "* Champs obligatoires";
+ legend.appendChild(hint);
+ this.inputs.push(legend);
+
+ // Code postal
+ const comCodeInput = this.comCodeInput = document.createElement("input");
+ comCodeInput.className = "fr-input";
+ comCodeInput.type = "text";
+ comCodeInput.name = "commCode";
+ comCodeInput.pattern = "^(\\d\\d|2[A,B,a,b,0-9])\\d{3}$";
+ comCodeInput.title = "Code postal ou code INSEE";
+ comCodeInput.id = Helper.getUid("ParcelAdvancedSearch-comCode-");
+ const codeDiv = this._getLabelContainer("Renseigner un lieu*", "fr-input-group", comCodeInput, "Code postal ou code INSEE");
+
+ // Autocomplete list
+ const autocompleteList = this.autocompleteList = document.createElement("ul");
+ autocompleteList.className = "GPautoCompleteList";
+ autocompleteList.id = Helper.getUid("GPautoCompleteList-");
+ autocompleteList.setAttribute("role", "listbox");
+ autocompleteList.setAttribute("tabindex", "-1");
+ autocompleteList.setAttribute("aria-label", "Propositions");
+ codeDiv.insertBefore(autocompleteList, comCodeInput.nextSibling);
+
+ // Input controller for accessibility
+ comCodeInput.setAttribute("role", "combobox");
+ comCodeInput.setAttribute("aria-controls", autocompleteList.id);
+ comCodeInput.setAttribute("aria-expanded", "false");
+ comCodeInput.setAttribute("aria-autocomplete", "list");
+ comCodeInput.setAttribute("aria-haspopup", "listbox");
+ comCodeInput.setAttribute("autocomplete", "off");
+
+ // Prefix
+ const prefixInput = this.prefixInput = document.createElement("select");
+ prefixInput.className = "fr-select";
+ prefixInput.name = "prefix";
+ prefixInput.title = "Choisir le préfixe de la parcelle";
+ prefixInput.id = Helper.getUid("ParcelAdvancedSearch-prefix-");
+ prefixInput.setAttribute("disabled", "disabled");
+ this._getLabelContainer("Préfix", "fr-select-group", prefixInput, "");
+
+ // Section
+ const sectionInput = this.sectionInput = document.createElement("select");
+ sectionInput.className = "fr-select";
+ sectionInput.name = "section";
+ sectionInput.title = "Choisir la section de la parcelle";
+ sectionInput.id = Helper.getUid("ParcelAdvancedSearch-section-");
+ sectionInput.setAttribute("disabled", "disabled");
+ this._getLabelContainer("Section*", "fr-select-group", sectionInput, "");
+
+ // Numero
+ const numberInput = this.numberInput = document.createElement("input");
+ numberInput.className = "fr-input";
+ numberInput.type = "text";
+ numberInput.pattern = "\\d{1,4}";
+ numberInput.maxLength = "4";
+ numberInput.name = "numero";
+ numberInput.title = "Numéro de la parcelle";
+ numberInput.autocomplete = "off";
+ numberInput.ariaExpanded = "false";
+ numberInput.id = Helper.getUid("ParcelAdvancedSearch-number-");
+ numberInput.setAttribute("disabled", "disabled");
+ const numDiv = this._getLabelContainer("Numéro*", "fr-input-group", numberInput, "");
+
+ const numberList = this.numberList = document.createElement("ul");
+ numberList.className = "GPautoCompleteList";
+ numberList.id = Helper.getUid("GPautoCompleteList-");
+ numberList.setAttribute("role", "listbox");
+ numberList.setAttribute("tabindex", "-1");
+ numberList.setAttribute("aria-label", "Propositions");
+ numDiv.insertBefore(numberList, numberInput.nextSibling);
+ }
+
+ /** Do search
+ * @protected
+ * @override
+ * @param {PointerEvent} e Événement de soumission
+ */
+ _onSearch (e) {
+ if (e) {
+ super._onSearch(e);
+ }
+ // Nothing to do if no commune selected
+ if (!this.communeId) {
+ return;
+ }
+
+ // Check form validity
+ this._clearMessages();
+
+ const prefix = this.prefixInput.value;
+ const section = this.sectionInput.value;
+ const number = this.numberInput.value;
+ if (!prefix) {
+ // this._showMessage("prefix", "Le préfixe est obligatoire.");
+ return;
+ } else if (!section) {
+ this._showMessage("section", "La section est obligatoire.");
+ return;
+ } else if (!this.numberInput.checkValidity()) {
+ this._showMessage("numero", "Le numéro de parcelle doit être un nombre.");
+ return;
+ } else if (!number) {
+ this._showMessage("numero", "Le numéro de parcelle est obligatoire.");
+ return;
+ }
+
+ // Gestion des arrondissements municipaux
+ const communeId = this.arrondId || this.communeId;
+ // Search parcelle
+ const parcelId = communeId
+ + "000".slice(prefix.length) + prefix
+ + section
+ + "0000".slice(number.length) + number;
+
+ this.searchService.search({
+ location : parcelId
+ });
+ }
+
+ /**
+ * Réinitialise les champs du formulaire.
+ * @param {PointerEvent} e Événement d'effacement
+ * @protected
+ */
+ _onErase (e) {
+ super._onErase(e);
+ this.setCommune();
+ this._clearMessages();
+ }
+
+}
+
+export default ParcelAdvancedSearch;
+
+// Expose ParcelAdvancedSearch as ol.control.ParcelAdvancedSearch (for a build bundle)
+if (window.ol && window.ol.control) {
+ window.ol.control.ParcelAdvancedSearch = ParcelAdvancedSearch;
+}
\ No newline at end of file
diff --git a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js
new file mode 100644
index 000000000..571612a8c
--- /dev/null
+++ b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js
@@ -0,0 +1,685 @@
+import Control from "../Control";
+import Geolocation from "ol/Geolocation";
+import OlFeature from "ol/Feature";
+import Point from "ol/geom/Point";
+import SearchEngineGeocodeIGN from "./SearchEngineGeocodeIGN";
+import Helper from "../../Utils/Helper";
+import Select, { SelectEvent } from "ol/interaction/Select";
+import Map from "ol/Map";
+
+import Vector from "ol/layer/Vector";
+import VectorSource from "ol/source/Vector";
+
+import Overlay from "ol/Overlay.js";
+
+import { Style, Icon, Stroke, Fill } from "ol/style";
+import mapPinIcon from "./map-pin-2-fill.svg";
+import Feature from "ol/Feature";
+import { Layer } from "ol/layer";
+import AbstractAdvancedSearch from "./AbstractAdvancedSearch";
+
+/** Get style for features
+ * @param {String|Array} color - Couleur du contour
+ * @param {String|Array} [fillColor] - Couleur de remplissage
+ * @param {Number} [offset = 0] - Décalage de la ligne. Par défaut, 0
+ * @returns {Style} Style OpenLayers
+ */
+function getStyle (color, fillColor, offset = 0) {
+ return new Style({
+ image : new Icon({
+ src : mapPinIcon,
+ color : color,
+ anchor : [0.5, 1],
+ }),
+ stroke : new Stroke({
+ color : color,
+ lineDash : [8, 8],
+ width : 2,
+ lineDashOffset : offset
+ }),
+ fill : new Fill({
+ color : fillColor || "rgba(0, 0, 0, 0.1)",
+ }),
+ });
+}
+
+/**
+ * @classdesc
+ * Contrôle de recherche avancée permettant de rechercher via d'autres manières.
+ * Gère aussi l'ajout des élements sur la carte etc.
+ *
+ * @extends {Control}
+ * @module SearchEngineAdvanced
+ */
+class SearchEngineAdvanced extends Control {
+
+ /**
+ * Constructeur du contrôle de recherche avancée.
+ * @param {SearchEngineAdvancedOptions} options - Options du constructeur.
+ */
+ constructor (options) {
+ options = options || {};
+ // call ol.control.Control constructor
+
+ super(options);
+
+ // Geolocation
+ this.geolocation = new Geolocation({
+ // enableHighAccuracy must be set to true to have the heading value.
+ trackingOptions : {
+ enableHighAccuracy : true,
+ },
+ projection : "EPSG:4326",
+ });
+
+
+ this.layer = new Vector({
+ source : new VectorSource({}),
+ zIndex : Infinity,
+ style : [getStyle([255, 255, 255, 1]), getStyle([0, 0, 145, 1], null, 8)],
+ });
+
+ this.selectInteraction = new Select({
+ layers : [this.layer],
+ style : getStyle([145, 0, 0, 1], [145, 0, 0, 0.2]),
+ });
+
+ // Initialize
+ this.initialize(options);
+ this._initContainer(options);
+ this._initEvents(options);
+
+ options.popupButtons = options.popupButtons ? options.popupButtons : [];
+
+ this.selectInteraction.on("select", this._onSelectElement.bind(this));
+ this.popup = this._createPopup(options.popupButtons);
+ }
+
+ /**
+ * Initialise les options du contrôle.
+ * @param {SearchEngineAdvancedOptions} options - Options du constructeur.
+ * @private
+ */
+ initialize (options) {
+ /**
+ * Nom de la classe (heritage)
+ * @private
+ */
+ this.CLASSNAME = "SearchEngineAdvanced";
+
+ /**
+ * @type {Array}
+ */
+ this._searchForms;
+
+ /**
+ * Si vrai, écoute les clics sur le document pour gérer
+ * la modale de recherche avancée
+ * @type {Boolean}
+ */
+ this.listenToClick = false;
+
+ if (options.advancedSearch && options.advancedSearch instanceof Array) {
+ this._searchForms = options.advancedSearch;
+ } else {
+ this._searchForms = [];
+ }
+
+ this._searchForms.forEach(search => {
+ // Gère la recherche
+ search.on("search", this.onAdvancedSearchResult.bind(this));
+ });
+ }
+
+ /**
+ * @override
+ * @param {Map|null} map Carte cible
+ */
+ setMap (map) {
+ if (this.getMap() && this.baseSearchEngine) {
+ this.getMap().removeControl(this.baseSearchEngine);
+ }
+ super.setMap(map);
+ if (this.baseSearchEngine) {
+ this.baseSearchEngine.setMap(map);
+ }
+ this._searchForms.forEach(search => {
+ search.setMap(map);
+ });
+
+ this.element.appendChild(this.advancedContainer);
+
+ if (map) {
+ // Place les couches au dessus des autres
+ this.layer.setMap(map);
+ map.addInteraction(this.selectInteraction);
+ map.addOverlay(this.popup);
+ }
+ }
+
+ /**
+ * Retourne la couche utilisée pour afficher les résultats.
+ * @returns {Layer} Couche des résultats
+ */
+ getLayer () {
+ return this.layer;
+ }
+
+ /**
+ * Initialise les événements du contrôle (géolocalisation, navigation clavier, recherche).
+ * @param {SearchEngineAdvancedOptions} options Options du constructeur.
+ * @private
+ */
+ _initEvents (options) {
+ this.geolocation.on("change:position", () => {
+ const pt = new Point(this.geolocation.getPosition());
+ pt.transform("EPSG:4326", this.getMap().getView().getProjection());
+ const evt = this.createEvent(pt, "Ma localisation");
+ this.addResultToMap(evt);
+ this.dispatchEvent(evt);
+ this.geolocation.setTracking(false);
+ });
+
+ this.baseSearchEngine.input.addEventListener("keydown", function (/** @type {KeyboardEvent} */ e) {
+ if (e.key === "Tab" && !e.shiftKey && this.locationBtn.checkVisibility()) {
+ e.preventDefault();
+ this.locationBtn.focus();
+ }
+ }.bind(this));
+
+ this.locationBtn.addEventListener("keydown", function (/** @type {KeyboardEvent} */ e) {
+ if (e.key === "Tab") {
+ e.preventDefault();
+ if (e.shiftKey) {
+ // Retourne sur l'input
+ this.baseSearchEngine.input.focus();
+ } else if (this.advancedBtn.checkVisibility()) {
+ // Focus sur le bouton de recherche avancée
+ this.advancedBtn.focus();
+ } else {
+ this.eraseBtn.focus();
+ }
+ }
+ }.bind(this));
+
+ this.on("search", this.addResultToMap.bind(this));
+
+ // Gère le cas du conteneur de recherche avancée
+ ["mousedown", "focusin"].map(eventListener => document.addEventListener(eventListener, this._onDocumentClick.bind(this)));
+
+ this.advancedBtn.addEventListener("blur", function (e) {
+ if (e.relatedTarget === this.baseSearchEngine.input) {
+ this.listenToClick = false;
+ this.advancedBtn.setAttribute("aria-expanded", false);
+ }
+ }.bind(this));
+
+ this.baseSearchEngine.input.addEventListener("blur", function (/** @type {FocusEvent} */e) {
+ if (e.relatedTarget && e.relatedTarget === this.eraseBtn) {
+ e.target.dispatchEvent(new Event("input"));
+ }
+ }.bind(this));
+ }
+
+ /**
+ * Crée un événement de recherche à partir d'un objet (Feature ou Point).
+ * @param {Object|Point|OlFeature} obj Objet à afficher (Feature ou Point)
+ * @param {String} [info] Texte affiché dans la popup
+ * @returns {Object} Événement normalisé de type "search"
+ */
+ createEvent (obj, info) {
+ let evt = obj;
+ if (obj instanceof OlFeature) {
+ evt = {
+ result : obj,
+ extent : null
+ };
+ } else if (obj instanceof Point) {
+ evt = {
+ result : new OlFeature(obj),
+ extent : null
+ };
+ }
+ if (info) {
+ evt.result.set("infoPopup", info);
+ }
+ evt.type = "search";
+ return evt;
+ }
+
+ /**
+ * Initialise le conteneur principal du contrôle et les sous-composants.
+ * @param {SearchEngineAdvancedOptions} options Options du constructeur
+ * @private
+ */
+ _initContainer (options) {
+ // Gestion de l'affichage des options avancées
+ const element = this.element = document.createElement("div");
+ element.className = "GPwidget gpf-widget";
+ element.id = Helper.getUid("GPsearchEngine-Advanced-");
+
+ // Default base search engine
+ // const baseContainer = this.baseContainer = document.createElement("div");
+ // this.element.appendChild(baseContainer);
+ options.target = this.element;
+ options.searchButton = true;
+ options.search = true;
+ this.baseSearchEngine = new SearchEngineGeocodeIGN(options);
+ this.baseSearchEngine.on(["select", "search", "autocomplete"], (e) => {
+ this.dispatchEvent(e);
+ });
+
+ // Geolocation
+ this.locationBtn = this._getGeolocButton();
+ this.baseSearchEngine.autocompleteHeader.appendChild(this.locationBtn);
+
+ // Ajout des options avancées
+ const advancedBtn = this.advancedBtn = document.createElement("button");
+ advancedBtn.className = "GPSearchEngine-advanced-btn fr-btn fr-btn--sm fr-icon-arrow-up-s-line fr-btn--icon-right fr-btn--tertiary-no-outline";
+ advancedBtn.id = Helper.getUid("GPSearchEngine-advanced-btn-");
+ advancedBtn.type = "button";
+ advancedBtn.title = "Avancée";
+ advancedBtn.innerHTML = "Avancée";
+ advancedBtn.setAttribute("aria-label", "Afficher les options avancées");
+ advancedBtn.setAttribute("aria-expanded", "false");
+
+ // Gestion de l'affichage des options avancées
+ const advancedContainer = this.advancedContainer = document.createElement("div");
+ advancedContainer.className = "GPAdvancedContainer";
+ advancedContainer.id = Helper.getUid("GPsearchEngine-AdvancedContainer-");
+ advancedContainer.setAttribute("aria-labelledby", advancedBtn.id);
+ // baseContainer.appendChild(advancedContainer);
+
+ // Geolocation
+ advancedContainer.appendChild(this._getGeolocButton());
+
+ // Formulaires specifiques
+ this._searchForms.forEach(Search => {
+ const section = document.createElement("section");
+ section.className = "fr-accordion";
+ advancedContainer.appendChild(section);
+ const title = document.createElement("h3");
+ title.className = "fr-accordion__title";
+ section.appendChild(title);
+ const button = document.createElement("button");
+ button.type = "button";
+ button.className = "fr-accordion__btn";
+ button.setAttribute("aria-expanded", "false");
+ button.innerText = Search.getName();
+ title.appendChild(button);
+ // Accordion
+ const accordion = document.createElement("div");
+ accordion.className = "fr-collapse";
+ accordion.id = Helper.getUid("accordion-");
+ button.setAttribute("aria-controls", accordion.id);
+ section.appendChild(accordion);
+
+ // Contenu recherche avancée
+ Search.setTarget(accordion);
+
+ button.addEventListener("click", () => {
+ const expanded = button.getAttribute("aria-expanded") === "true";
+ advancedContainer.querySelectorAll("section").forEach(sec => {
+ sec.querySelector(".fr-collapse").classList.remove("fr-collapse--expanded");
+ sec.querySelector("button").setAttribute("aria-expanded", "false");
+ advancedContainer.dataset.open = !expanded;
+ if (!expanded) {
+ sec.classList.add("fr-hidden");
+ } else {
+ sec.classList.remove("fr-hidden");
+ }
+ });
+ if (!expanded) {
+ button.setAttribute("aria-expanded", "true");
+ accordion.classList.add("fr-collapse--expanded");
+ section.classList.remove("fr-hidden");
+ }
+ Search.dispatchEvent({ type : "expand", expanded : !expanded });
+ });
+ });
+
+ // Gestion du bouton avancé
+ advancedBtn.setAttribute("aria-controls", advancedContainer.id);
+ advancedBtn.addEventListener("click", function (/** @type {PointerEvent} */ e) {
+ e.preventDefault();
+ const isHidden = advancedBtn.getAttribute("aria-expanded") === "false";
+ advancedBtn.setAttribute("aria-expanded", isHidden);
+ this.listenToClick = isHidden;
+ if (isHidden) {
+ // Si la modale est ouverte, on met le focus sur le premier élément focusable
+ const focusableSelectors = [
+ "a[href]",
+ "button:not([disabled])",
+ "input:not([disabled])",
+ "select:not([disabled])",
+ "textarea:not([disabled])",
+ "[tabindex]:not([tabindex='-1'])"
+ ].join(",");
+ const firstFocusable = advancedContainer.querySelector(focusableSelectors);
+ if (firstFocusable) {
+ firstFocusable.focus();
+ }
+ }
+ }.bind(this));
+
+ // N'ajoute pas le bouton s'il n'y a pas d'options avancées
+ if (this._searchForms.length) {
+ this.baseSearchEngine.optionscontainer.appendChild(advancedBtn);
+ }
+
+ // Ajout des options avancées
+ const eraseBtn = this.eraseBtn = document.createElement("button");
+ eraseBtn.className = "GPSearchEngine-erase-btn fr-btn fr-btn--sm fr-icon-close-circle-fill fr-btn--tertiary-no-outline";
+ eraseBtn.id = Helper.getUid("GPSearchEngine-erase-btn-");
+ eraseBtn.type = "button";
+ eraseBtn.title = "Effacer la saisie";
+ eraseBtn.setAttribute("aria-label", "Effacer la saisie");
+ // Gestion du bouton avancé
+ eraseBtn.addEventListener("click", function () {
+ this.baseSearchEngine.input.value = "";
+ delete this.baseSearchEngine.input.dataset.empty;
+ // Notifie l'input du changement
+ this.baseSearchEngine.input.dispatchEvent(new Event("input"));
+ // Met le focus sur l'input
+ console.log("click");
+ setTimeout(() => {
+ this.baseSearchEngine.input.focus();
+ }, 50);
+ }.bind(this));
+ this.baseSearchEngine.optionscontainer.appendChild(eraseBtn);
+ }
+
+ /**
+ * Fonction active si la recherche avancée est active
+ * @param {PointerEvent} e Événement de clic sur le document
+ */
+ _onDocumentClick (e) {
+ if (this.listenToClick === true) {
+ // Écoute des clics sur le document ==> recherche avancée active
+ const clickOnAdvancedContainer = (this.advancedContainer === e.target || this.advancedContainer.contains(e.target));
+ const clickOnAdvancedBtn = this.advancedBtn === e.target;
+ if (!(clickOnAdvancedContainer || clickOnAdvancedBtn)) {
+ // On fait une action si un clic se produit en dehors du conteneur
+ // Et si le bouton de recherche avancée n'est pas cliqué
+ this.listenToClick = false;
+ this.advancedBtn.setAttribute("aria-expanded", false);
+ }
+ }
+ }
+
+ /**
+ * Ajoute les résultats (features) sur la carte et ajuste la vue.
+ * @param {Object} e Événement de recherche contenant result/extent
+ */
+ addResultToMap (e) {
+ this._closePopup();
+ this.layer.getSource().clear();
+ let extent;
+ if (!!e.result) {
+ this.layer.getSource().addFeature(e.result);
+ extent = e.result.getGeometry().getExtent();
+ this.selectInteraction.getFeatures().push(e.result);
+ this._setPopupInfo(e.result);
+ }
+ if (!!e.extent) {
+ this.layer.getSource().addFeature(e.extent);
+ extent = e.extent.getGeometry().getExtent();
+ }
+ if (this.getMap()) {
+ let view = this.getMap().getView();
+ if (extent) {
+ view.fit(extent);
+ if (view.getZoom() > 15) {
+ view.setZoom(15);
+ }
+ }
+ }
+ }
+
+ /**
+ * Ajoute les infos au popup
+ * @param {Feature} [feature] Feature à ajouter. Si non fourni
+ * @param {Number[]} [position] Position du popup
+ */
+ _setPopupInfo (feature, position) {
+ if (feature) {
+ // Ferme l'ancien popup
+ this.popup.setPosition(undefined);
+ let offset = null;
+ // Ajoute le popup
+ if (feature.getGeometry()?.getType() === "Point") {
+ // Place le popup sur le point
+ position = feature.getGeometry()?.getCoordinates();
+ // TODO : AMÉLIORER L'OFFSET
+ offset = [0, -20];
+ }
+ this.popup.setPosition(position);
+ offset && this.popup.setOffset(offset);
+ this.setPopupContent(feature.get("infoPopup") || "");
+ this.popup.set("feature", feature);
+ this.popup.set("layer", this.layer);
+ } else {
+ this.popup.setPosition(undefined);
+ this.setPopupContent("");
+ this.popup.unset("feature");
+ this.popup.unset("layer");
+ }
+ }
+
+ /**
+ * Callback lors de la sélection d'une feature (affiche le popup).
+ * @param {SelectEvent} e Événement de sélection
+ * @private
+ */
+ _onSelectElement (e) {
+ let position = e.mapBrowserEvent.coordinate;
+ this._setPopupInfo(e.selected.length ? e.selected[0] : null, position);
+ }
+
+ /**
+ * Crée et retourne l'overlay popup pour afficher les infos de feature.
+ * @private
+ * @param {PopupButton[]} popupButtons - Bouton à ajouter dans le popup (en plus de la suppression / fermeture).
+ * @returns {Overlay} Overlay du popups
+ */
+ _createPopup (popupButtons) {
+ // Popup global
+ let element = this._popupDiv = document.createElement("div");
+ // TODO : ajouter gp-feature-info-div lorsque les deux seront pareils
+ element.className = "GPSearchPopup";
+
+ // Contenu du popup
+ let popupContent = this._popupContent = document.createElement("div");
+ popupContent.className = "GPPopupContent";
+
+ // Groupe de boutons
+ let popupBtns = this._popupBtns = document.createElement("div");
+ popupBtns.className = "GPButtonGroups gpf-btns-group";
+
+ popupBtns.appendChild(this._addCloseButton());
+ popupBtns.appendChild(this._addRemoveButton());
+
+ popupButtons.forEach(popupBtn => {
+ popupBtns.appendChild(this._createCustomPopupButton(popupBtn));
+ });
+
+ element.appendChild(popupContent);
+ element.appendChild(popupBtns);
+
+ const overlay = new Overlay({
+ element : element,
+ positioning : "bottom-center",
+ });
+
+ return overlay;
+ }
+
+ /**
+ * Définit le contenu HTML du popup.
+ * @param {String} content Contenu HTML à afficher
+ */
+ setPopupContent (content) {
+ this._popupContent.innerHTML = content;
+ }
+
+ /**
+ * Crée le bouton de fermeture du popup.
+ * @returns {HTMLButtonElement} Bouton de fermeture
+ * @private
+ */
+ _addCloseButton () {
+ let closer = document.createElement("button");
+ closer.title = closer.ariaLabel = "Fermer la pop-up";
+ closer.textContent = "Fermer";
+ closer.className = "GPButton gpf-btn fr-icon-close-line fr-btn fr-btn--sm gpf-btn--tertiary fr-btn--tertiary-no-outline";
+
+ // Ferme le popup
+ closer.onclick = this._closePopup.bind(this);
+ return closer;
+ }
+
+ /**
+ * Ferme le popup et désélectionne la feature.
+ * @returns {Boolean} false
+ * @private
+ */
+ _closePopup () {
+ this.selectInteraction.getFeatures().clear();
+ if (this.popup !== null) {
+ this.popup.setPosition(undefined);
+ }
+ return false;
+ }
+
+ /**
+ * Crée le bouton de suppression du marqueur.
+ * @returns {HTMLButtonElement} Bouton de suppression
+ * @private
+ */
+ _addRemoveButton () {
+ let remove = document.createElement("button");
+ remove.title = remove.ariaLabel = "Supprimer le marqueur";
+ remove.textContent = "Supprimer";
+ remove.className = "GPButton gpf-btn fr-icon-delete-line fr-btn fr-btn--sm gpf-btn--tertiary fr-btn--tertiary-no-outline";
+
+ // Supprime la feature
+ remove.onclick = this._removeFeature.bind(this);
+
+ return remove;
+ }
+
+ /**
+ * Supprime la feature sélectionnée de la couche et ferme le popup.
+ * @private
+ */
+ _removeFeature () {
+ const f = this.popup.get("feature");
+ const layer = this.popup.get("layer");
+ // Supprime la feature
+ if (layer && f) {
+ layer.getSource().removeFeature(f);
+
+ this.dispatchEvent({
+ type : this.REMOVE_FEATURE_EVENT,
+ feature : f,
+ layer : layer,
+ });
+
+ // Ferme le popup
+ this._closePopup();
+ }
+ }
+
+ /**
+ * Crée un bouton personnalisé pour le popup.
+ * @param {PopupButton} popupButton - Configuration du bouton.
+ * @returns {HTMLButtonElement} Bouton HTML
+ */
+ _createCustomPopupButton (popupButton) {
+ const btn = document.createElement("button");
+ btn.title = btn.ariaLabel = popupButton.label;
+ btn.className = "GPButton fr-btn fr-btn--sm fr-btn--tertiary-no-outline ";
+ if (popupButton.className) {
+ btn.className += popupButton.className;
+ }
+ if (popupButton.icon) {
+ btn.classList.add(popupButton.icon);
+ }
+ if (popupButton.attributes) {
+ Object.entries(popupButton.attributes).forEach(([key, value]) => {
+ btn.setAttribute(key, value);
+ });
+ }
+ btn.onclick = () => {
+ const feature = this.popup.get("feature");
+ if (feature && typeof popupButton.onClick === "function") {
+ // New feature sans style
+ const newFeature = feature.clone();
+ newFeature.setStyle(undefined);
+ // Appel du callback
+ if (popupButton.onClick.call(this, newFeature)) {
+ // Feature traitée => supprimer de la sélection
+ this._closePopup();
+ this.selectInteraction.getFeatures().clear();
+ this.layer.getSource().removeFeature(feature);
+ };
+ }
+ };
+ return btn;
+ }
+
+ /**
+ * Crée le bouton de géolocalisation.
+ * @returns {HTMLButtonElement} Bouton de géolocalisation
+ * @private
+ */
+ _getGeolocButton () {
+ const locationBtn = document.createElement("button");
+ locationBtn.innerText = "Me géolocaliser";
+ locationBtn.className = "GPSearchEngine-locate fr-btn fr-btn--sm fr-icon-crosshair-2-line fr-btn--icon-left fr-btn--tertiary-no-outline";
+ locationBtn.addEventListener("click", () => {
+ this.geolocation.setTracking(true);
+ console.log("tracking", this.geolocation);
+ });
+ return locationBtn;
+ }
+
+ /**
+ * Callback lors d'un résultat de recherche avancée.
+ * @param {Object} e Événement de recherche avancée
+ * @private
+ */
+ onAdvancedSearchResult (e) {
+ // Ferme la modale de recherche avancée
+ this.listenToClick = false;
+ this.advancedBtn.setAttribute("aria-expanded", false);
+ // Ferme les sections
+ this.closeAllSections();
+ if (e.result instanceof Array) {
+ // TODO : GÉRER MULTIPLE RÉSULTATS
+ } else if (e.result instanceof Feature) {
+ this.addResultToMap(e);
+ }
+ }
+
+ /**
+ * Ferme toutes les accordéons et affiche les sections
+ */
+ closeAllSections () {
+ this.advancedContainer.dataset.open = false;
+ this.advancedContainer.querySelectorAll("section").forEach(section => {
+ const btn = section.querySelector(".fr-accordion__title > button[aria-expanded]");
+ btn.setAttribute("aria-expanded", "false");
+ btn.ariaControlsElements[0].classList.remove("fr-collapse--expanded");
+ section.classList.remove("fr-hidden");
+ });
+ }
+
+}
+
+export default SearchEngineAdvanced;
+
+// Expose SearchEngine as ol.control.SearchEngine (for a build bundle)
+if (window.ol && window.ol.control) {
+ window.ol.control.SearchEngineAdvanced = SearchEngineAdvanced;
+}
\ No newline at end of file
diff --git a/src/packages/Controls/SearchEngine/SearchEngineBase.js b/src/packages/Controls/SearchEngine/SearchEngineBase.js
new file mode 100644
index 000000000..8a0c6ad66
--- /dev/null
+++ b/src/packages/Controls/SearchEngine/SearchEngineBase.js
@@ -0,0 +1,703 @@
+// import CSS
+import "../../CSS/Controls/SearchEngine/GPFsearchEngine.css";
+import Control from "../Control";
+import Logger from "../../Utils/LoggerByDefault";
+import DefaultSearchService from "../../Services/DefaultSearchService";
+import Helper from "../../Utils/Helper";
+
+// Voir les typedefs partagés dans ./typedefs.js (SearchEngineBaseOptions, SearchServiceOptions, ...)
+
+const history = "fr-icon-history-line";
+const defaultIcon = "fr-icon-map-pin-2-line";
+const typeClasses = {
+ "StreetAddress" : "fr-icon-map-pin-2-line",
+ "PositionOfInterest" : {
+ "administratif" : "fr-icon-france-line",
+ "hydrographie" : "fr-icon-ign-mer",
+ "transport" : {
+ "télécabine, téléphérique transport par câble" : "fr-icon-subway-line",
+ "gare voyageurs uniquement" : "fr-icon-subway-line",
+ "voyageurs et fret" : "fr-icon-subway-line",
+ "station de métro" : "fr-icon-subway-line",
+ "gare fret uniquement" : "fr-icon-subway-line",
+ "gare routière" : "fr-icon-subway-line",
+ "station de tramway" : "fr-icon-subway-line",
+ "arrêt voyageurs" : "fr-icon-subway-line",
+
+ "aérodrome" : "fr-icon-plane-line",
+ "héliport" : "fr-icon-plane-line",
+ "altiport" : "fr-icon-plane-line",
+ "aérogare" : "fr-icon-plane-line",
+
+ "port" : "fr-icon-ship-2-line",
+ "gare maritime" : "fr-icon-ship-2-line",
+
+ "parking" : "fr-icon-parking-box-line",
+ "aire de repos ou de service" : "fr-icon-parking-box-line",
+
+ "service dédié aux vélos" : "fr-icon-bike-line",
+ }
+ }
+};
+
+var logger = Logger.getLogger("searchengine");
+
+/**
+ * @classdesc
+ * Contrôle de base pour la recherche (barre de recherche, autocomplétion, historique).
+ *
+ * @alias ol.control.SearchEngineBase
+ * @module SearchEngine
+ */
+class SearchEngineBase extends Control {
+
+ /**
+ * Constructeur du contrôle SearchEngineBase.
+ * @constructor
+ * @param {SearchEngineBaseOptions} options Options du constructeur
+ * @fires autocomplete
+ * @fires search
+ * @fires select
+ * @example
+ * const search = new ol.control.SearchEngineBase({
+ * placeholder: "Rechercher une adresse...",
+ * minChars: 3,
+ * maximumEntries: 10,
+ * historic: "mesRecherches",
+ * searchService: new CustomSearchService()
+ * });
+ * map.addControl(search)
+ */
+ constructor (options) {
+ options = options || {};
+ // call ol.control.Control constructor
+ super(options);
+
+ /**
+ * Nom de la classe (heritage)
+ * @private
+ */
+ this.CLASSNAME = "SearchEngineBase";
+
+ // initialisation du composant
+ this.initialize(options);
+
+ this.searchService = options.searchService;
+ // Permet l'autocomplétion
+ if (this.searchService.get("autocomplete") !== false) {
+ this.searchService.on("autocomplete", function (e) {
+ this.onAutocomplete(e);
+ }.bind(this));
+ }
+
+ this.searchService.on("search", function (e) {
+ this.onSearch(e);
+ }.bind(this));
+
+ // Widget main DOM container
+ this._initContainer(options);
+
+ this._initEvents(options);
+
+ // Get historic in localStorage
+ this._historic = false;
+ this._historicName = "GPsearch-" + options.historic;
+ if (options.historic !== false && this.searchService.get("autocomplete") !== false) {
+ this._historic = [];
+ try {
+ const stor = window.localStorage.getItem(this._historicName);
+ if (stor) {
+ this._historic = JSON.parse(stor);
+ }
+ } catch (e) {
+ // logger.warn("LocalStorage not available");
+ }
+ }
+ this.showHistoric();
+ }
+ /**
+ * Initialise le contrôle SearchEngineBase (appelé par le constructeur).
+ * @protected
+ * @param {SearchEngineBaseOptions} options Options du constructeur
+ */
+ initialize (options) {
+ // Valeurs par défaut des options
+ options.minChars = options.minChars ? options.minChars : 3;
+ options.maximumEntries = (typeof options.maximumEntries === "number") ? options.maximumEntries : 5;
+ options.historic = (typeof options.historic === "string" ? options.historic : this.CLASSNAME);
+ options.title = options.title ? options.title : "Rechercher";
+ options.ariaLabel = options.ariaLabel ? options.ariaLabel : "Rechercher";
+ options.placeholder = options.placeholder ? options.placeholder : "";
+ options.searchService = options.searchService ? options.searchService : new DefaultSearchService();
+ options.label = options.label ? options.label : "";
+ options.hint = options.hint ? options.hint : "";
+ options.search = options.search === true ? true : false;
+ options.searchButton = options.searchButton === true ? true : false;
+ options.collapsible = options.collapsible === true ? true : false;
+
+ this.set("maximumEntries", options.maximumEntries);
+ }
+
+ /**
+ * Ajoute les écouteurs d'événements sur les éléments du contrôle.
+ * @protected
+ * @param {SearchEngineBaseOptions} options Options du constructeur
+ */
+ _initEvents (options) {
+ if (this.searchService.get("autocomplete") !== false) {
+ // Empty input
+ this.input.addEventListener("input", function (e) {
+ if (e.target.value.length > 0) {
+ e.target.dataset.erase = true;
+ } else {
+ delete e.target.dataset.erase;
+ }
+ console.log("input", e, e.target.value);
+ if (!e.target.value) {
+ this.showHistoric();
+ }
+ }.bind(this));
+ // Prevent cursor to go to the end of input on keydown
+ this.input.addEventListener("keydown", function (e) {
+ if (/ArrowDown|ArrowUp/.test(e.key)) {
+ e.preventDefault();
+ }
+ }.bind(this));
+ // Keyboard navigation
+ this.input.addEventListener("keyup", function (e) {
+ // autocomplete list
+ const list = Array.from(this.autocompleteList.querySelectorAll("li"));
+ let idx = list.findIndex(li => li.classList.contains("active"));
+ if (idx === -1) {
+ // Ancienne valeur
+ this._previousValue = e.target.value;
+ }
+ switch (e.key) {
+ case "ArrowDown":
+ case "ArrowUp":
+ e.preventDefault();
+ // Navigation in autocomplete list
+ if (list.length === 0) {
+ return;
+ }
+ list.forEach(li => li.classList.remove("active"));
+ if (e.key === "ArrowDown") {
+ idx++;
+ if (idx >= list.length) {
+ idx = -1;
+ }
+ } else if (e.key === "ArrowUp") {
+ idx--;
+ if (idx < -1) {
+ idx = list.length - 1;
+ }
+ }
+ if (idx !== -1) {
+ // Set active
+ const current = list[idx];
+ current.classList.add("active");
+ this.input.value = current.innerText;
+ this.input.setAttribute("aria-activedescendant", current.id);
+ this.input.setAttribute("data-active-option", current.id);
+ } else {
+ // Réaffiche la valeur précédente de l'utilisateur
+ e.target.value = this._previousValue;
+ }
+ // Envoie un événement de type input pour notifier le changement
+ this.input.dispatchEvent(new Event("input"));
+
+ break;
+ default:
+ if (e.target.value.length && e.target.value.length >= options.minChars && e.target.value !== this._currentValue) {
+ this.autocomplete(e.target.value);
+ }
+ break;
+ }
+ this._currentValue = e.target.value;
+ }.bind(this), false);
+ }
+
+ // Événement d'envoi du formulaire
+ this.container.addEventListener("submit", function (e) {
+ e.preventDefault();
+ const list = Array.from(this.autocompleteList.querySelectorAll("li"));
+
+ if (e && e.submitter && e.submitter.type === "submit") {
+ // Si on appuie sur le bouton, on vérifie que l'input ne soit pas vide
+ let input = e.target.querySelector("input");
+ const value = input.value;
+ if (value.length < options.minChars) {
+ return false;
+ }
+ }
+ let idx = list.findIndex(li => li.classList.contains("active"));
+ let item = list[idx];
+ if (idx < 0) {
+ // Pas d'item sélectionné : on prend le premier de la liste
+ item = list[0];
+ }
+ if (item) {
+ item.click();
+ }
+ }.bind(this));
+ }
+ /**
+ * Initialise le conteneur DOM principal du contrôle.
+ * @private
+ * @param {SearchEngineBaseOptions} options Options du constructeur
+ * @returns {void}
+ */
+ _initContainer (options) {
+ const element = this.element = document.createElement("div");
+ element.className = "GPwidget gpf-widget";
+ element.id = Helper.getUid("GPsearchEngine-");
+ // Main container
+ const container = this.container = document.createElement("form");
+ // container.className = options.search ? "GPSearchBar fr-search-bar" : "";
+ container.className = options.search ? "GPSearchBar" : "";
+ // container.className = "fr-search-bar";
+ container.id = Helper.getUid("GPsearchInput-Base-");
+
+ // Création du bouton
+ if (!options.target && options.collapsible) {
+ this.button = document.createElement("button");
+ this.button.id = Helper.getUid("GPshowSearchEnginePicto-");
+ this.button.className = "GPshowOpen GPshowAdvancedToolPicto GPshowSearchEnginePicto gpf-btn fr-icon-search-line fr-btn fr-btn--lg";
+ this.button.setAttribute("aria-pressed", "true");
+ // this.button.setAttribute("type", "submit");
+ // this.button.setAttribute("form", container.id);
+ if (options.title) {
+ this.button.setAttribute("title", options.title);
+ }
+ if (options.collapsible) {
+ this.button.addEventListener("click", function () {
+ element.classList.toggle("ol-collapsed");
+ const pressed = this.button.getAttribute("aria-pressed") === "true";
+ this.button.setAttribute("aria-pressed", !pressed);
+ if (!pressed) {
+ input.focus();
+ } else {
+ input.blur();
+ }
+ }.bind(this));
+ }
+ element.appendChild(this.button);
+ }
+
+ element.appendChild(container);
+
+ const search = document.createElement("div");
+ // search.className = "GPInputGroup fr-input";
+ search.className = "GPInputGroup";
+ search.classList.add(options.search ? "fr-input" : "fr-input-group");
+ container.appendChild(search);
+
+ // Input
+ const input = this.input = document.createElement("input");
+ input.type = "text";
+ input.className = "GPsearchInputText fr-input";
+ input.id = Helper.getUid("GPsearchInputText-");
+ input.placeholder = options.placeholder;
+ input.autocomplete = "off";
+ input.setAttribute("aria-label", options.ariaLabel);
+
+ if (options.label) {
+ const label = document.createElement("label");
+ label.className = "GPLabel fr-label";
+ label.textContent = options.label;
+ label.htmlFor = input.id;
+ if (options.hint) {
+ const hint = document.createElement("span");
+ hint.className = "GPLabelHint fr-hint-text";
+ hint.textContent = options.hint;
+ label.appendChild(hint);
+ }
+ search.appendChild(label);
+ }
+ search.appendChild(input);
+
+ const messages = document.createElement("div");
+ messages.className = "GPMessagesGroup fr-messages-group";
+ messages.ariaLive = "polite";
+ messages.id = Helper.getUid("GPMessagesGroup-");
+ input.setAttribute("aria-describedby", messages.id);
+ search.appendChild(messages);
+
+ // Options container
+ this.optionscontainer = document.createElement("div");
+ this.optionscontainer.className = "GPOptionsContainer";
+ search.appendChild(this.optionscontainer);
+
+ // Submit button
+ if (options.searchButton) {
+ const submit = this.subimtBt = document.createElement("button");
+ submit.className = "GPsearchInputSubmit gpf-btn fr-icon-search-line fr-btn";
+ submit.id = Helper.getUid("GPshowSearchEnginePicto-");
+ submit.type = "submit";
+ if (options.title) {
+ submit.setAttribute("title", options.title);
+ }
+ container.appendChild(submit);
+ }
+
+ // Autocomplete container
+ const acContainer = this.acContainer = document.createElement("div");
+ acContainer.id = Helper.getUid("GPautoCompleteContainer-");
+ acContainer.className = "GPautoCompleteContainer GPelementHidden gpf-hidden";
+ element.appendChild(acContainer);
+ // element.appendChild(acContainer);
+
+ // Autocomplete list
+ const autocompleteHeader = this.autocompleteHeader = document.createElement("div");
+ autocompleteHeader.className = "GPautoCompleteHeader";
+ acContainer.appendChild(autocompleteHeader);
+
+ const autocompleteList = this.autocompleteList = document.createElement("ul");
+ autocompleteList.className = "GPautoCompleteList";
+ autocompleteList.id = Helper.getUid("GPautoCompleteList-");
+ autocompleteList.setAttribute("role", "listbox");
+ autocompleteList.setAttribute("tabindex", "-1");
+ autocompleteList.setAttribute("aria-label", "Propositions");
+ acContainer.appendChild(autocompleteList);
+
+ const autocompleteFooter = this.autocompleteFooter = document.createElement("div");
+ autocompleteFooter.className = "GPautoCompleteFooter";
+ acContainer.appendChild(autocompleteFooter);
+
+ // Input controller for accessibility
+ input.setAttribute("role", "combobox");
+ input.setAttribute("aria-controls", acContainer.id);
+ input.setAttribute("aria-expanded", "false");
+ input.setAttribute("aria-autocomplete", "list");
+ input.setAttribute("aria-haspopup", "listbox");
+
+ if (this.searchService.get("autocomplete") !== false) {
+ input.addEventListener("focus", () => {
+ input.setAttribute("aria-expanded", "true");
+ acContainer.classList.add("gpf-visible");
+ acContainer.classList.remove("gpf-hidden");
+ acContainer.classList.add("GPelementVisible");
+ acContainer.classList.remove("GPelementHidden");
+ });
+
+ // Gère le focus pour la sélection d'éléments dans la liste
+ input.addEventListener("blur", (e) => {
+ // N'agit que si le focus est hors de l'élément
+ if (e.relatedTarget && acContainer.contains(e.relatedTarget)) {
+ // N'empêche pas le focus sur un bouton
+ if (!(e.relatedTarget.tagName === "BUTTON")) {
+ input.focus();
+ } else {
+ // Ajout d'un event listener pour retourner sur l'input en cas de besoin
+ e.relatedTarget.addEventListener("blur", (/** @type {FocusEvent}*/ e) => {
+ if (e.relatedTarget && acContainer.contains(e.relatedTarget) || e.relatedTarget === input) {
+ input.focus();
+ } else {
+ // On doit aller sur le bouton recherche avancée
+ if (input.value.length === 0) {
+ delete input.dataset.erase;
+ e.relatedTarget.parentNode.querySelector("button")?.focus();
+ }
+ setTimeout(() => {
+ input.setAttribute("aria-expanded", "false");
+ acContainer.classList.remove("gpf-visible");
+ acContainer.classList.add("gpf-hidden");
+ acContainer.classList.remove("GPelementVisible");
+ acContainer.classList.add("GPelementHidden");
+ }, 50);
+ }
+ }, { once : true });
+ }
+ } else {
+ input.value.length === 0 && delete input.dataset.erase;
+ setTimeout(() => {
+ input.setAttribute("aria-expanded", "false");
+ acContainer.classList.remove("gpf-visible");
+ acContainer.classList.add("gpf-hidden");
+ acContainer.classList.remove("GPelementVisible");
+ acContainer.classList.add("GPelementHidden");
+ }, 50);
+ }
+ });
+ }
+ }
+
+ /**
+ * Active ou désactive le contrôle (désactive l'input / bouton).
+ * @param {Boolean} active Indique si le contrôle doit être désactivé
+ * @returns {void}
+ */
+ setActive (active) {
+ this.input.disabled = !!active;
+ this.subimtBt.disabled = !!active;
+ }
+
+ /**
+ * Lance l'autocomplétion et met à jour la liste.
+ * @param {String} [value] Valeur de l'input
+ * @api
+ */
+ autocomplete (value) {
+ clearTimeout(this._completeDelay);
+ this._completeDelay = setTimeout(function () {
+ this.searchService.autocomplete(value);
+ }.bind(this), this.get("triggerDelay") || 100);
+ }
+
+ /**
+ * Callback sur événement d'autocomplétion.
+ * @param {Object} e Événement d'autocomplétion
+ * @private
+ */
+ onAutocomplete (e) {
+ clearTimeout(this._completeDelay);
+ // Update list}
+ this._updateList(e.result);
+ this.dispatchEvent(e);
+ }
+
+ /**
+ * Lance la recherche de géocodage.
+ * @param {IGNSearchObject} item Valeur ou objet à rechercher
+ * @api
+ */
+ search (item) {
+ clearTimeout(this._completeDelay);
+ this._completeDelay = setTimeout(function () {
+ this.searchService.search(item);
+ }.bind(this), this.get("triggerDelay") || 100);
+ }
+
+ /**
+ * Callback sur événement de recherche.
+ * @param {Object} e Événement de recherche
+ * @api
+ */
+ onSearch (e) {
+ clearTimeout(this._completeDelay);
+ // Update list}
+ this.dispatchEvent(e);
+ }
+
+ /**
+ * Callback sur sélection d'un item.
+ * @param {Object} item Élément sélectionné
+ * @api
+ */
+ select (item) {
+ clearTimeout(this._completeDelay);
+ const title = this.getItemTitle(item);
+ this.input.value = title;
+ this.input.dispatchEvent(new Event("input"));
+ this._currentValue = title;
+ this._updateHistoric(item);
+ this._updateList();
+ this.dispatchEvent({
+ type : "select",
+ title : this.getItemTitle(item),
+ item : item
+ });
+ }
+
+ /**
+ * Affiche la liste de l'historique.
+ * @api
+ */
+ showHistoric () {
+ clearTimeout(this._completeDelay);
+ if (this._historic) {
+ this._updateList(this._historic.length ? this._historic : [], "history");
+ }
+ }
+
+ /**
+ * Met à jour la liste d'autocomplétion.
+ * @private
+ * @param {Array