diff --git a/ui/package.json b/ui/package.json index 7c0ace54..194d58fd 100644 --- a/ui/package.json +++ b/ui/package.json @@ -34,6 +34,7 @@ "react-autocomplete-hint": "^2.0.0", "react-color": "^2.19.3", "react-dom": "^18.3.1", + "react-html-parser": "^2.0.2", "react-i18next": "^15.1.3", "react-map-gl": "~7.1.7", "react-markdown": "^9.0.1", @@ -84,6 +85,7 @@ "@types/react": "^18.3.12", "@types/react-color": "^3.0.12", "@types/react-dom": "^18.3.1", + "@types/react-html-parser": "^2", "@types/react-router-dom": "^5.3.3", "@types/semver": "^7.5.8", "@types/uuid": "^10.0.0", diff --git a/ui/src/views/map/Map.tsx b/ui/src/views/map/Map.tsx index 9e3a4de5..dadda41c 100644 --- a/ui/src/views/map/Map.tsx +++ b/ui/src/views/map/Map.tsx @@ -40,6 +40,7 @@ import LayerControl from "./controls/LayerControl"; import MapboxDraw from "@mapbox/mapbox-gl-draw"; import maplibregl from "maplibre-gl"; import classNames from "classnames"; +import SearchControl from "./controls/Searchbox"; const modes = { ...MapboxDraw.modes, @@ -77,6 +78,7 @@ function MapView() { reuseMaps={false} RTLTextPlugin={undefined} > + {/* All Map Controls */} diff --git a/ui/src/views/map/controls/Searchbox.tsx b/ui/src/views/map/controls/Searchbox.tsx new file mode 100644 index 00000000..3af9f9df --- /dev/null +++ b/ui/src/views/map/controls/Searchbox.tsx @@ -0,0 +1,129 @@ +import { useState, useCallback } from "react"; +import classNames from "classnames"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faSearch } from "@fortawesome/free-solid-svg-icons"; +import { useMap } from "react-map-gl/maplibre"; +import debounce from "lodash/debounce"; +import isEmpty from "lodash/isEmpty"; +import ReactHtmlParser from "react-html-parser"; + +const BASE_URL = "https://api3.geo.admin.ch/rest/services/api/SearchServer"; + +interface SearchResult { + bbox: number[]; + features: SearchFeature[]; +} + +interface SearchFeature { + bbox: number[]; + geometry: { + coordinates: number[]; + type: string; + }; + id: number | string; + properties: { + detail: string; + label: string; + rank: number; + type: string; + geom_quadindex: string; + lat: number; + lon: number; + objectclass: string; + origin: string; + weight: number; + x: number; + y: number; + zoomlevel: number; + }; +} + +function SearchControl() { + const { current: map } = useMap(); + const [searchResults, setSearchResults] = useState([]); + const [input, setInput] = useState(""); + + const flyTo = useCallback( + (target: SearchFeature) => { + map?.flyTo({ + center: [target.properties.lon, target.properties.lat], + zoom: 17, + animate: true, + duration: 2500, + }); + setSearchResults([]); + setInput(""); + }, + [map], + ); + + const search = (input: string) => { + fetch( + BASE_URL + + "?" + + new URLSearchParams({ + searchText: input, + type: "locations", + geometryFormat: "geojson", + origins: "address,gazetteer,parcel", + limit: "10", + }), + ) + .then((response) => response.json()) + .then((data) => { + const searchResult: SearchResult = { + bbox: data.bbox, + features: data.features, + }; + setSearchResults(searchResult.features); + }); + }; + const debouncedSearch = debounce(search, 1000); + + const onChange = (e: React.ChangeEvent) => { + const value = e.target.value; + setInput(value); + debouncedSearch(value); + }; + + const dropdown = classNames({ + dropdown: true, + "is-active": !isEmpty(searchResults), + }); + + return ( + + + + + + + + + e.key === "Enter" && search(input)} + /> + + + + + {searchResults && + searchResults.map((result: SearchFeature) => ( + flyTo(result)} key={result.id} className="dropdown-item"> + {ReactHtmlParser(result.properties.label)} + + ))} + + + + + + ); +} + +export default SearchControl; diff --git a/ui/yarn.lock b/ui/yarn.lock index 2c7befac..e962bd3c 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -4328,6 +4328,22 @@ __metadata: languageName: node linkType: hard +"@types/domhandler@npm:^2.4.0, @types/domhandler@npm:^2.4.3": + version: 2.4.5 + resolution: "@types/domhandler@npm:2.4.5" + checksum: 10/0dd551361c0ec4e8736a6bcb142ca3c9eedd9b8076cded263c555c0e4adef9b07230dc85b7c94e5cd72e9796ea2533c00e4bcf97d5e06bc10e32367429985ff1 + languageName: node + linkType: hard + +"@types/domutils@npm:*": + version: 1.7.8 + resolution: "@types/domutils@npm:1.7.8" + dependencies: + "@types/domhandler": "npm:^2.4.0" + checksum: 10/ac97a3c2baf6c7ec93a3e54a30e36363dc4acaaf76b36e8b7dc5a06d626f48ce1d4beb9cd9767c1ce745825b78296528b60a128627c324c66eaec6616dc2ea42 + languageName: node + linkType: hard + "@types/eslint-config-prettier@npm:^6.11.3": version: 6.11.3 resolution: "@types/eslint-config-prettier@npm:6.11.3" @@ -4456,6 +4472,18 @@ __metadata: languageName: node linkType: hard +"@types/htmlparser2@npm:*": + version: 3.10.7 + resolution: "@types/htmlparser2@npm:3.10.7" + dependencies: + "@types/domhandler": "npm:^2.4.3" + "@types/domutils": "npm:*" + "@types/node": "npm:*" + domhandler: "npm:^2.4.0" + checksum: 10/74fbd402f554ac529cc7ff1aedbdc05eabd13694a636ae91cf5842c7fe8d1a0c4253c9400b957203f01d8564615771296d22f85132be894f71eb4254dd81adb6 + languageName: node + linkType: hard + "@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1": version: 2.0.4 resolution: "@types/istanbul-lib-coverage@npm:2.0.4" @@ -4644,6 +4672,16 @@ __metadata: languageName: node linkType: hard +"@types/react-html-parser@npm:^2": + version: 2.0.6 + resolution: "@types/react-html-parser@npm:2.0.6" + dependencies: + "@types/htmlparser2": "npm:*" + "@types/react": "npm:*" + checksum: 10/6c7568c55313be6e79370ac82bf35d78925d81535492716be50ddd7eb8c1a9f9c25223bc731843cb679155b74a53111d2136dbd0aac31ccb83a43167f46d53d6 + languageName: node + linkType: hard + "@types/react-router-dom@npm:^5.3.3": version: 5.3.3 resolution: "@types/react-router-dom@npm:5.3.3" @@ -6674,6 +6712,39 @@ __metadata: languageName: node linkType: hard +"dom-serializer@npm:0": + version: 0.2.2 + resolution: "dom-serializer@npm:0.2.2" + dependencies: + domelementtype: "npm:^2.0.1" + entities: "npm:^2.0.0" + checksum: 10/376344893e4feccab649a14ca1a46473e9961f40fe62479ea692d4fee4d9df1c00ca8654811a79c1ca7b020096987e1ca4fb4d7f8bae32c1db800a680a0e5d5e + languageName: node + linkType: hard + +"domelementtype@npm:1, domelementtype@npm:^1.3.1": + version: 1.3.1 + resolution: "domelementtype@npm:1.3.1" + checksum: 10/7893da40218ae2106ec6ffc146b17f203487a52f5228b032ea7aa470e41dfe03e1bd762d0ee0139e792195efda765434b04b43cddcf63207b098f6ae44b36ad6 + languageName: node + linkType: hard + +"domelementtype@npm:^2.0.1": + version: 2.3.0 + resolution: "domelementtype@npm:2.3.0" + checksum: 10/ee837a318ff702622f383409d1f5b25dd1024b692ef64d3096ff702e26339f8e345820f29a68bcdcea8cfee3531776b3382651232fbeae95612d6f0a75efb4f6 + languageName: node + linkType: hard + +"domhandler@npm:^2.3.0, domhandler@npm:^2.4.0": + version: 2.4.2 + resolution: "domhandler@npm:2.4.2" + dependencies: + domelementtype: "npm:1" + checksum: 10/d8b0303c53c0eda912e45820ef8f6023f8462a724e8b824324f27923970222a250c7569e067de398c4d9ca3ce0f2b2d2818bc632d6fa72956721d6729479a9b9 + languageName: node + linkType: hard + "dompurify@npm:^2.5.4": version: 2.5.7 resolution: "dompurify@npm:2.5.7" @@ -6681,6 +6752,16 @@ __metadata: languageName: node linkType: hard +"domutils@npm:^1.5.1": + version: 1.7.0 + resolution: "domutils@npm:1.7.0" + dependencies: + dom-serializer: "npm:0" + domelementtype: "npm:1" + checksum: 10/8c1d879fd3bbfc0156c970d12ebdf530f541cbda895d7f631b2444d22bbb9d0e5a3a4c3210cffb17708ad67531d7d40e1bef95e915c53a218d268607b66b63c8 + languageName: node + linkType: hard + "dot-case@npm:^3.0.4": version: 3.0.4 resolution: "dot-case@npm:3.0.4" @@ -6781,6 +6862,20 @@ __metadata: languageName: node linkType: hard +"entities@npm:^1.1.1": + version: 1.1.2 + resolution: "entities@npm:1.1.2" + checksum: 10/4a707022f4e932060f03df2526be55d085a2576fe534421e5b22bc62abb0d1f04241c171f9981e3d7baa4f4160606cad72a2f7eb01b6a25e279e3f31a2be4bf2 + languageName: node + linkType: hard + +"entities@npm:^2.0.0": + version: 2.2.0 + resolution: "entities@npm:2.2.0" + checksum: 10/2c765221ee324dbe25e1b8ca5d1bf2a4d39e750548f2e85cbf7ca1d167d709689ddf1796623e66666ae747364c11ed512c03b48c5bbe70968d30f2a4009509b7 + languageName: node + linkType: hard + "entities@npm:^4.4.0": version: 4.4.0 resolution: "entities@npm:4.4.0" @@ -8434,6 +8529,20 @@ __metadata: languageName: node linkType: hard +"htmlparser2@npm:^3.9.0": + version: 3.10.1 + resolution: "htmlparser2@npm:3.10.1" + dependencies: + domelementtype: "npm:^1.3.1" + domhandler: "npm:^2.3.0" + domutils: "npm:^1.5.1" + entities: "npm:^1.1.1" + inherits: "npm:^2.0.1" + readable-stream: "npm:^3.1.1" + checksum: 10/d5297fe76c0d6b0f35f39781417eb560ef12fa121953578083f3f2b240c74d5c35a38185689d181b6a82b66a3025436f14aa3413b94f3cd50ba15733f2f72389 + languageName: node + linkType: hard + "http-cache-semantics@npm:^4.1.0": version: 4.1.1 resolution: "http-cache-semantics@npm:4.1.1" @@ -8641,7 +8750,7 @@ __metadata: languageName: node linkType: hard -"inherits@npm:2, inherits@npm:^2.0.3": +"inherits@npm:2, inherits@npm:^2.0.1, inherits@npm:^2.0.3": version: 2.0.4 resolution: "inherits@npm:2.0.4" checksum: 10/cd45e923bee15186c07fa4c89db0aace24824c482fb887b528304694b2aa6ff8a898da8657046a5dcf3e46cd6db6c61629551f9215f208d7c3f157cf9b290521 @@ -11951,6 +12060,17 @@ __metadata: languageName: node linkType: hard +"react-html-parser@npm:^2.0.2": + version: 2.0.2 + resolution: "react-html-parser@npm:2.0.2" + dependencies: + htmlparser2: "npm:^3.9.0" + peerDependencies: + react: ^0.14.0 || ^15.0.0 || ^16.0.0-0 + checksum: 10/b4d9c5faf5c0add929ef7364b451db4ac9b9b10dd0f671f7316ce4486e42a45f674e59183effa1b08c3fb3ddb70f6dbdc07c1f6d882aeb65b54c586dfe205b9f + languageName: node + linkType: hard + "react-i18next@npm:^15.1.3": version: 15.1.3 resolution: "react-i18next@npm:15.1.3" @@ -12073,6 +12193,17 @@ __metadata: languageName: node linkType: hard +"readable-stream@npm:^3.1.1": + version: 3.6.2 + resolution: "readable-stream@npm:3.6.2" + dependencies: + inherits: "npm:^2.0.3" + string_decoder: "npm:^1.1.1" + util-deprecate: "npm:^1.0.1" + checksum: 10/d9e3e53193adcdb79d8f10f2a1f6989bd4389f5936c6f8b870e77570853561c362bee69feca2bbb7b32368ce96a85504aa4cedf7cf80f36e6a9de30d64244048 + languageName: node + linkType: hard + "readable-stream@npm:^3.6.0": version: 3.6.0 resolution: "readable-stream@npm:3.6.0" @@ -13004,6 +13135,7 @@ __metadata: "@types/react": "npm:^18.3.12" "@types/react-color": "npm:^3.0.12" "@types/react-dom": "npm:^18.3.1" + "@types/react-html-parser": "npm:^2" "@types/react-router-dom": "npm:^5.3.3" "@types/semver": "npm:^7.5.8" "@types/uuid": "npm:^10.0.0" @@ -13038,6 +13170,7 @@ __metadata: react-autocomplete-hint: "npm:^2.0.0" react-color: "npm:^2.19.3" react-dom: "npm:^18.3.1" + react-html-parser: "npm:^2.0.2" react-i18next: "npm:^15.1.3" react-map-gl: "npm:~7.1.7" react-markdown: "npm:^9.0.1"