diff --git a/cityinfra/settings.py b/cityinfra/settings.py index 190b5ba7..1250d28b 100644 --- a/cityinfra/settings.py +++ b/cityinfra/settings.py @@ -64,6 +64,7 @@ HELUSERS_ADGROUPS_CLAIM=(str, "groups"), LOGGING_AUTH_DEBUG=(bool, False), BASEMAP_SOURCE_URL=(str, "https://kartta.hel.fi/ws/geoserver/avoindata/wms"), + ADDRESS_SEARCH_BASE_URL=(str, "https://api.hel.fi/servicemap/v2/search"), STATIC_URL=(str, "/static/"), MEDIA_URL=(str, "/media/"), CSRF_COOKIE_SECURE=(bool, True), @@ -377,6 +378,7 @@ SRID_BOUNDARIES = {3879: (25487917.144, 6645439.071, 25514074.175, 6687278.623)} BASEMAP_SOURCE_URL = env.str("BASEMAP_SOURCE_URL") +ADDRESS_SEARCH_BASE_URL = env.str("ADDRESS_SEARCH_BASE_URL") # Import / Export IMPORT_EXPORT_USE_TRANSACTIONS = True diff --git a/map-view/package.json b/map-view/package.json index 5c5a8d41..3a6497f3 100644 --- a/map-view/package.json +++ b/map-view/package.json @@ -20,6 +20,7 @@ "i18next-browser-languagedetector": "^6.0.1", "ol": "^10.5.0", "prettier": "^3.5.3", + "proj4": "^2.19.10", "react": "^19.1.0", "react-dom": "^19.1.0", "react-i18next": "^15.3.1", diff --git a/map-view/src/App.tsx b/map-view/src/App.tsx index 5c3e516b..4e9cb70b 100644 --- a/map-view/src/App.tsx +++ b/map-view/src/App.tsx @@ -3,6 +3,7 @@ import Fab from "@mui/material/Fab"; import { Theme } from "@mui/material/styles"; import { createStyles, withStyles, WithStyles } from "@mui/styles"; import LayersIcon from "@mui/icons-material/Layers"; +import SearchIcon from "@mui/icons-material/Search"; import "ol/ol.css"; import React from "react"; import MapConfigAPI from "./api/MapConfigAPI"; @@ -12,17 +13,25 @@ import FeatureInfo from "./components/FeatureInfo"; import Map from "./common/Map"; import { Feature, MapConfig } from "./models"; import OngoingFetchInfo from "./components/OngoingFetchInfo"; +import AddressInput from "./components/AddressInput"; +import { Address, convertToEPSG3879OL, getAddressSearchResults, getNameFromAddress } from "./common/AddressSearchUtils"; const drawWidth = "400px"; const styles = (theme: Theme) => createStyles({ - mapButton: { + layerSwitcherButton: { position: "absolute", right: "16px", top: "16px", color: "white", }, + searchButton: { + position: "absolute", + left: "50px", + top: "16px", + color: "white", + }, drawer: { width: drawWidth, }, @@ -35,9 +44,11 @@ interface AppProps extends WithStyles {} interface AppState { open: boolean; + openAddressSearch: boolean; mapConfig: MapConfig | null; features: Feature[]; ongoingFeatureFetches: Set; + addressSearchResults: Address[]; } class App extends React.Component { @@ -47,9 +58,11 @@ class App extends React.Component { super(props); this.state = { open: false, + openAddressSearch: false, mapConfig: null, features: [], ongoingFeatureFetches: new Set(), + addressSearchResults: [], }; } @@ -66,9 +79,30 @@ class App extends React.Component { }); } + handleSearch = async (address: string) => { + const addressSearchResults = await getAddressSearchResults(Map.getAddressSearchUrl(address)); + this.setState({ addressSearchResults }); + }; + + clearSearchResults = () => { + this.setState({ addressSearchResults: [] }); + }; + + handleSelect = (result: Address) => { + if (result?.location?.coordinates) { + const coordinates = convertToEPSG3879OL(result.location.coordinates); + Map.clearSelectedAddressLayer(); + Map.markSelectedAddress(coordinates, getNameFromAddress(result) || "Address name not found"); + Map.centerToCoordinates(coordinates); + } else { + console.error("No valid coordinates found for selected address."); + } + this.setState({ addressSearchResults: [] }); + }; + render() { const { classes } = this.props; - const { open, mapConfig, features, ongoingFeatureFetches } = this.state; + const { open, openAddressSearch, mapConfig, features, ongoingFeatureFetches, addressSearchResults } = this.state; return (
@@ -86,6 +120,37 @@ class App extends React.Component { }} /> )} + { + Map.showSelectedAddressLayer(!openAddressSearch); + this.setState({ openAddressSearch: !openAddressSearch }); + }} + className={classes.searchButton} + > + + + + { + Map.showSelectedAddressLayer(false); + this.setState({ openAddressSearch: false }); + }} + onSearch={this.handleSearch} + onSelect={this.handleSelect} + clearResults={this.clearSearchResults} + results={addressSearchResults} + /> + {ongoingFeatureFetches.size > 0 && ( )} @@ -93,7 +158,7 @@ class App extends React.Component { size="medium" color="primary" onClick={() => this.setState({ open: !open })} - className={classes.mapButton} + className={classes.layerSwitcherButton} > diff --git a/map-view/src/api/__mocks__/mock-data.ts b/map-view/src/api/__mocks__/mock-data.ts index 721196c5..13403b19 100644 --- a/map-view/src/api/__mocks__/mock-data.ts +++ b/map-view/src/api/__mocks__/mock-data.ts @@ -48,6 +48,7 @@ export const mockMapConfig: MapConfig = { imageUrl: "testurl", }, traffic_sign_icons_url: "http://127.0.0.1:8000/static/traffic_control/svg/traffic_sign_icons/", + address_search_base_url: "http://127.0.0.1:8000/foo", featureTypeEditNameMapping: {}, icon_scale: 0.1, icon_type: "svg", diff --git a/map-view/src/common/AddressSearchUtils.ts b/map-view/src/common/AddressSearchUtils.ts new file mode 100644 index 00000000..2cba58bf --- /dev/null +++ b/map-view/src/common/AddressSearchUtils.ts @@ -0,0 +1,184 @@ +import { fromLonLat, addCoordinateTransforms, get as getProjection, addProjection, Projection } from "ol/proj"; +import proj4 from "proj4"; + +// Define the EPSG:3879 projection string using proj4's definition function. +// This tells proj4 what the projection is. This must be done before anything else. +proj4.defs( + "EPSG:3879", + "+proj=tmerc +lat_0=0 +lon_0=25 +k=1 +x_0=25500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs", +); + +// Register the proj4 projection with OpenLayers. +addProjection( + new Projection({ + code: "EPSG:3879", + units: "m", + }), +); + +// Get the projection objects for both the source (WGS 84) and destination (EPSG:3879). +const wgs84Projection = getProjection("EPSG:4326"); +const epsg3879Projection = getProjection("EPSG:3879"); + +// Check that both projections exist before adding the transform. +if (wgs84Projection && epsg3879Projection) { + // Add the coordinate transformation between EPSG:4326 and EPSG:3879. + addCoordinateTransforms( + wgs84Projection, + epsg3879Projection, + // Forward transform from EPSG:4326 to EPSG:3879 + function (coordinate) { + return proj4("EPSG:4326", "EPSG:3879", coordinate); + }, + // Inverse transform from EPSG:3879 to EPSG:4326 + function (coordinate) { + return proj4("EPSG:3879", "EPSG:4326", coordinate); + }, + ); +} else { + console.error("Failed to get one or both projection objects for coordinate transformation."); +} + +/** + * + */ +export function buildAddressSearchQuery(address: string) { + return new URLSearchParams({ + q: address, + type: "address", + municipality: "helsinki", + language: getResolvedLanguageCode() || "fi", + }).toString(); +} + +interface Location { + type: string; + coordinates: [number, number]; +} + +interface Municipality { + id: string; + name: { + fi: string; + sv: string; + }; +} + +interface Street { + name: { + fi: string; + sv: string; + }; +} + +export interface Address { + object_type: string; + name: { + fi: string; + sv: string; + en: string; + }; + number: string; + number_end: string; + letter: string; + modified_at: string; + municipality: Municipality; + street: Street; + location: Location; +} + +interface ApiResponse { + count: number; + next: string | null; + previous: string | null; + results: Address[]; +} + +/** + * Transforms geographic coordinates from WGS 84 (EPSG:4326) to EPSG:3879 + * using the OpenLayers fromLonLat function. + * * @param {number[]} wgs84Coords - The coordinates in WGS 84 format [longitude, latitude]. + * @returns {number[]} The transformed coordinates in EPSG:3879 format [x, y], only 1 page of results are used. + */ +export function convertToEPSG3879OL(wgs84Coords: [number, number]): [number, number] { + const transformed = fromLonLat(wgs84Coords, "EPSG:3879"); + return transformed as [number, number]; +} + +export async function getAddressSearchResults(url: string): Promise { + console.log("Fetching results from:", url); + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const data: ApiResponse = await response.json(); + return data.results || []; + } catch (error) { + console.error("Error fetching search results:", error); + return []; + } +} + +/** + * Extracts the language code from the URL path. + * Assumes the language code is the first segment after the domain (e.g., http://localhost:8000/en/map/). + * + * @returns {string | undefined} The language code found in the URL, or undefined if not present. + */ +export function getLanguageCodeFromUrl(): string | undefined { + const pathSegments = window.location.pathname.split("/"); + // The first segment after the root '/' is at index 1. + const langCode = pathSegments[1]; + return langCode; +} + +/** + * Resolves the language code by first checking the URL and then falling back to the browser settings. + * + * @returns {string | undefined} The resolved language code, or undefined if none is found. + */ +export function getResolvedLanguageCode(): string | undefined { + const urlLang = getLanguageCodeFromUrl(); + if (urlLang) { + return urlLang; + } + + const browserLang = getBrowserLanguageCode(); + return browserLang; +} + +/** + * Extracts the primary language code from the user's browser settings. + * For example, it will return 'en' from 'en-US'. + * + * @returns {string | undefined} The language code from the browser, or undefined if not available. + */ +export function getBrowserLanguageCode(): string | undefined { + // navigator.language returns a string like 'en-US' or 'fi-FI' + // .split('-')[0] extracts the primary language code (e.g., 'en' or 'fi') + return navigator.language.split("-")[0]; +} + +/** + * Gets the localized name from an Address object based on the resolved language code. + * + * @param {Address} address - The Address object containing the name variants. + * @returns {string | undefined} The name in the resolved language, or undefined if no match is found. + */ +export function getNameFromAddress(address: Address): string | undefined { + const { name } = address; + + // 1. Get the resolved language code + const resolvedLang = getResolvedLanguageCode(); + if (resolvedLang && name[resolvedLang as keyof typeof name]) { + return name[resolvedLang as keyof typeof name]; + } + + // 2. Optional: Fallback to a default language if no match is found + if (name.en) { + return name.en; + } + + return undefined; +} diff --git a/map-view/src/common/Map.ts b/map-view/src/common/Map.ts index 6f61c83f..d1b3fca1 100644 --- a/map-view/src/common/Map.ts +++ b/map-view/src/common/Map.ts @@ -25,6 +25,7 @@ import { Cluster } from "ol/source"; import BaseObject from "ol/Object"; import { Extent, getCenter } from "ol/extent"; import { + getAddressMarkerStyle, getDiffLayerIdentifier, getDiffLayerIdentifierFromFeature, getHighlightStyle, @@ -35,6 +36,7 @@ import { import Static from "ol/source/ImageStatic"; import { bboxPolygon, booleanIntersects, union, featureCollection } from "@turf/turf"; import { Feature as TurfFeature, Polygon as TurfPolygon, MultiPolygon as TurfMultiPolygon, BBox } from "geojson"; +import { buildAddressSearchQuery } from "./AddressSearchUtils"; type TurfPolygonFeature = TurfFeature; function debounce void>(func: T, wait: number): (...args: Parameters) => void { @@ -100,6 +102,11 @@ class Map { */ private highLightedFeatureLayer: VectorLayer; + /** + * A layer to draw selected address marker + */ + private selectedAddressFeatureLayer: VectorLayer; + /** * Callback function to process features returned from GetFeatureInfo requests * @@ -147,6 +154,7 @@ class Map { const nonClusteredOverlayLayerGroup = this.createNonClusteredOverlayLayerGroup(mapConfig); this.planOfRealVectorLayer = Map.createPlanOfRealVectorLayer(); this.highLightedFeatureLayer = Map.createHighLightLayer(); + this.selectedAddressFeatureLayer = Map.createSelectedAddressLayer(); const planRealDiffVectorLayerGroup = this.createPlanRealDiffVectorLayerGroup(mapConfig); const resolutions = [256, 128, 64, 32, 16, 8, 4, 2, 1, 0.5, 0.25, 0.125, 0.0625]; @@ -168,11 +176,13 @@ class Map { planRealDiffVectorLayerGroup, this.highLightedFeatureLayer, this.planOfRealVectorLayer, + this.selectedAddressFeatureLayer, ], controls: this.getControls(), view, }); this.highLightedFeatureLayer.setVisible(true); + this.selectedAddressFeatureLayer.setVisible(true); this.overViewMap = new OverviewMap({ className: "ol-overviewmap", layers: [ @@ -360,6 +370,18 @@ class Map { this.highLightedFeatureLayer.getSource()?.addFeature(olFeature); } + /** + * + */ + markSelectedAddress(coords: [number, number], address: string) { + const olFeature = new OlFeature({ + geometry: new Point(coords), + name: "selectedAddress", + }); + this.selectedAddressFeatureLayer.setStyle(getAddressMarkerStyle(address)); + this.selectedAddressFeatureLayer.getSource()?.addFeature(olFeature); + } + showAllPlanAndRealDifferences(realLayer: VectorLayer, planLayer: VectorLayer) { let realFeatures: FeatureLike[], planFeatures: FeatureLike[] = []; @@ -418,6 +440,13 @@ class Map { }); } + private static createSelectedAddressLayer() { + const selectedAddressSource = new VectorSource({}); + return new VectorLayer({ + source: selectedAddressSource, + }); + } + private static createPlanRealDiffVectorLayer() { const planRealDiffVectorLayerSource = new VectorSource({}); return new VectorLayer({ @@ -684,6 +713,14 @@ class Map { this.highLightedFeatureLayer.getSource()!.clear(); } + clearSelectedAddressLayer() { + this.selectedAddressFeatureLayer.getSource()!.clear(); + } + + showSelectedAddressLayer(show: boolean) { + this.selectedAddressFeatureLayer.setVisible(show); + } + applyProjectFilters(overlayConfig: LayerConfig, projectId: string) { const { sourceUrl } = overlayConfig; const filter_field = "responsible_entity_name"; @@ -704,6 +741,22 @@ class Map { } } + centerToCoordinates(coords: Array) { + this.map.once("moveend", () => { + this.updateVisibleLayers(); + }); + this.map.getView().animate({ + center: coords, + zoom: 10, + duration: 2000, + }); + } + + getAddressSearchUrl(address: string) { + const searchUrlWithParams = buildAddressSearchQuery(address); + return `${this.mapConfig.address_search_base_url}?${searchUrlWithParams}`; + } + private createBasemapLayerGroup(basemapConfig: LayerConfig) { const { layers, sourceUrl } = basemapConfig; const basemapLayers = layers.map(({ identifier }, index) => { diff --git a/map-view/src/common/MapUtils.ts b/map-view/src/common/MapUtils.ts index 58f431a2..c00297d7 100644 --- a/map-view/src/common/MapUtils.ts +++ b/map-view/src/common/MapUtils.ts @@ -1,5 +1,5 @@ import { Circle, Geometry, GeometryCollection, LineString, MultiPolygon, Point, Polygon } from "ol/geom"; -import { Fill, Icon, Stroke, Style, Circle as CircleStyle } from "ol/style"; +import { Fill, Icon, Stroke, Style, Circle as CircleStyle, Text } from "ol/style"; import { FeatureLike } from "ol/Feature"; import RenderFeature from "ol/render/Feature"; import { Coordinate } from "ol/coordinate"; @@ -133,11 +133,32 @@ export function getSinglePointStyle( feature.get("device_type_icon"), ); } - const geometry = feature.getGeometry(); return getStylesForGeometry(geometry); } +export function getAddressMarkerStyle(note: string) { + return new Style({ + image: new CircleStyle({ + radius: 5, + fill: defaultFill, + stroke: defaultStroke, + }), + text: new Text({ + text: note, + font: "14px Calibri,sans-serif", + fill: new Fill({ + color: "black", + }), + stroke: new Stroke({ + color: "white", + width: 3, + }), + offsetY: -15, // Position the text above the marker + }), + }); +} + function getIconSrc( traffic_sign_icons_url: string, icon_type: string, diff --git a/map-view/src/components/AddressInput.module.css b/map-view/src/components/AddressInput.module.css new file mode 100644 index 00000000..84ebf2c5 --- /dev/null +++ b/map-view/src/components/AddressInput.module.css @@ -0,0 +1,18 @@ +.addressInputContainer { + position: relative; + top: 16px; + left: 5px; + z-index: 1000; + width: 90%; + padding: 16px; + background-color: white; + border-radius: 12px; + box-shadow: + 0 4px 6px rgba(0, 0, 0, 0.1), + 0 1px 3px rgba(0, 0, 0, 0.08); + border: 1px solid #e2e8f0; +} + +.selectedItem { + background-color: #e0e0e0; /* A light grey background for highlighting */ +} diff --git a/map-view/src/components/AddressInput.tsx b/map-view/src/components/AddressInput.tsx new file mode 100644 index 00000000..da3970d3 --- /dev/null +++ b/map-view/src/components/AddressInput.tsx @@ -0,0 +1,130 @@ +import React, { useState } from "react"; +import { + TextField, + Container, + Typography, + Box, + List, + ListItem, + ListItemText, + AppBar, + Toolbar, + IconButton, +} from "@mui/material"; +import CloseIcon from "@mui/icons-material/Close"; +import styles from "./AddressInput.module.css"; +import { Address, getNameFromAddress } from "../common/AddressSearchUtils"; +import { useTranslation } from "react-i18next"; + +interface AddressInputProps { + readonly onSearch: (address: string) => void; + readonly onSelect: (result: Address) => void; + readonly onClose: () => void; + readonly clearResults: () => void; + readonly results: Address[]; +} + +function AddressInput({ onSearch, onSelect, onClose, clearResults, results }: AddressInputProps) { + const { t } = useTranslation(); + const [address, setAddress] = useState(""); + const [open, setOpen] = useState(false); + const [selectedIndex, setSelectedIndex] = useState(-1); + + const handleChange = (event: React.ChangeEvent) => { + const value = event.target.value; + setAddress(value); + setSelectedIndex(-1); + setOpen(true); + // Only call onSearch if the length of the input value is 3 or more + if (value.length >= 3) { + onSearch(value); + } else { + clearResults(); + } + }; + + const handleSelect = (result: Address) => { + setAddress(getNameFromAddress(result)); + onSelect(result); + setOpen(false); + }; + + const handleFocus = () => { + setOpen(true); + }; + + const handleBlur = () => { + setTimeout(() => { + setOpen(false); + }, 200); // Small delay to allow click to register + }; + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === "ArrowDown") { + event.preventDefault(); + setSelectedIndex((prevIndex) => Math.min(prevIndex + 1, results.length - 1)); + } else if (event.key === "ArrowUp") { + event.preventDefault(); + setSelectedIndex((prevIndex) => Math.max(prevIndex - 1, -1)); + } else if (event.key === "Enter" && selectedIndex >= 0) { + event.preventDefault(); + handleSelect(results[selectedIndex]); + } else if (event.key === "Tab") { + // If there are results, prevent default behavior and focus the first list item + if (results.length > 0) { + event.preventDefault(); + setSelectedIndex(0); // Focus on the first itemkey= + } + } + }; + + return ( +
+ + + + {t("Address search")} + + + + + + + + + + {results.length > 0 && open && ( + + {results.map((result, index) => ( + handleSelect(result)} + sx={{ + cursor: "pointer", + backgroundColor: index === selectedIndex ? "#e0e0e0" : "transparent", + }} + > + + + ))} + + )} + + +
+ ); +} + +export default AddressInput; diff --git a/map-view/src/locale/en.json b/map-view/src/locale/en.json index 5b2b1b06..9ce9f044 100644 --- a/map-view/src/locale/en.json +++ b/map-view/src/locale/en.json @@ -13,6 +13,9 @@ "Display Plan/Real difference": "Display Plan/Real difference", "Filter by Project": "Filter by Project", "Active data fetches": "Active data fetches", - "No active data fetches": "No active data fetches" + "No active data fetches": "No active data fetches", + "Address": "Address", + "e.g. Vaasankatu 20": "e.g. Vaasankatu 20", + "Address search": "Address search" } } diff --git a/map-view/src/locale/fi.json b/map-view/src/locale/fi.json index 047c2595..0e996c47 100644 --- a/map-view/src/locale/fi.json +++ b/map-view/src/locale/fi.json @@ -13,6 +13,9 @@ "Display Plan/Real difference": "Näytä suunnitelman ja toteuman etäisyys", "Filter by Project": "Suodata projektin perusteella", "Active data fetches": "Aktiiviset tietojen haut", - "No active data fetches.": "Ei aktiivisia tietojen hakuja" + "No active data fetches": "Ei aktiivisia tietojen hakuja", + "Address": "Osoite", + "e.g. Vaasankatu 20": "esim. Vaasankatu 20", + "Address search": "Osoite haku" } } diff --git a/map-view/src/locale/sv.json b/map-view/src/locale/sv.json index 6765e3e6..f0702a5a 100644 --- a/map-view/src/locale/sv.json +++ b/map-view/src/locale/sv.json @@ -13,6 +13,9 @@ "Display Plan/Real difference": "Visa plan/implementering skillnad", "Filter by Project": "Filtrera efter projekt", "Active data fetches": "Aktiva datahämtningar", - "No active data fetches.": "Inga aktiva datahämtningar." + "No active data fetches.": "Inga aktiva datahämtningar.", + "Address": "Adress", + "e.g. Vaasankatu 20": "t.ex Gustav Vasas väg 20", + "Address search": "Adress sök" } } diff --git a/map-view/src/models.ts b/map-view/src/models.ts index 97532f38..9b7c0e27 100644 --- a/map-view/src/models.ts +++ b/map-view/src/models.ts @@ -31,6 +31,7 @@ export interface MapConfig { overlayConfig: LayerConfig; overviewConfig: OverviewConfig; traffic_sign_icons_url: string; + address_search_base_url: string; featureTypeEditNameMapping: Record; icon_scale: number; icon_type: string; diff --git a/map-view/yarn.lock b/map-view/yarn.lock index 54875f8a..f5858260 100644 --- a/map-view/yarn.lock +++ b/map-view/yarn.lock @@ -5667,6 +5667,11 @@ merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== +mgrs@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/mgrs/-/mgrs-1.0.0.tgz#fb91588e78c90025672395cb40b25f7cd6ad1829" + integrity sha512-awNbTOqCxK1DBGjalK3xqWIstBZgN6fxsMSiXLs9/spqWkF2pAhb2rrYCFSsr1/tT7PhcDGjZndG8SWYn0byYA== + micromatch@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" @@ -6015,6 +6020,14 @@ pretty-format@^27.0.2: ansi-styles "^5.0.0" react-is "^17.0.1" +proj4@^2.19.10: + version "2.19.10" + resolved "https://registry.yarnpkg.com/proj4/-/proj4-2.19.10.tgz#ca2a51627d4dfa20f68f3babed3380f8ef626262" + integrity sha512-uL6/C6kA8+ncJAEDmUeV8PmNJcTlRLDZZa4/87CzRpb8My4p+Ame4LhC4G3H/77z2icVqcu3nNL9h5buSdnY+g== + dependencies: + mgrs "1.0.0" + wkt-parser "^1.5.1" + prop-types@^15.6.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" @@ -7055,6 +7068,11 @@ why-is-node-running@^2.3.0: siginfo "^2.0.0" stackback "0.0.2" +wkt-parser@^1.5.1: + version "1.5.2" + resolved "https://registry.yarnpkg.com/wkt-parser/-/wkt-parser-1.5.2.tgz#a8eaf86ac2cc1d0a2e6a8082a930f5c7ebdb5771" + integrity sha512-1ZUiV1FTwSiSrgWzV9KXJuOF2BVW91KY/mau04BhnmgOdroRQea7Q0s5TVqwGLm0D2tZwObd/tBYXW49sSxp3Q== + word-wrap@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" diff --git a/map/views.py b/map/views.py index 7875bc87..56e40df8 100644 --- a/map/views.py +++ b/map/views.py @@ -68,6 +68,7 @@ def map_config(request): "icon_scale": get_icons_scale(), "icon_type": get_icons_type(), "featureTypeEditNameMapping": FeatureTypeEditMapping.get_featuretype_edit_name_mapping(), + "address_search_base_url": settings.ADDRESS_SEARCH_BASE_URL, } return JsonResponse(config)