Better map

This commit is contained in:
cracklesparkle
2024-10-29 15:08:23 +09:00
parent 115c6ec417
commit f51835584d
20 changed files with 685 additions and 444 deletions

View File

@ -6,7 +6,6 @@ import View from 'ol/View'
import { Draw, Modify, Select, Snap, Translate } from 'ol/interaction'
import { ImageStatic, OSM, Vector as VectorSource, XYZ } from 'ol/source'
import { Tile as TileLayer, Vector as VectorLayer } from 'ol/layer'
import { Stack, Typography } from '@mui/material'
import { Type } from 'ol/geom/Geometry'
import { click, never, noModifierKeys, platformModifierKeyOnly, primaryAction, shiftKeyOnly } from 'ol/events/condition'
import Feature from 'ol/Feature'
@ -24,27 +23,30 @@ import { Coordinate } from 'ol/coordinate'
import { Stroke, Fill, Circle as CircleStyle, Style, Text } from 'ol/style'
import { calculateCenter, calculateExtent, calculateRotationAngle, rotateProjection } from './mapUtils'
import MapBrowserEvent from 'ol/MapBrowserEvent'
import { fromLonLat, get, transform } from 'ol/proj'
import { get, transform } from 'ol/proj'
import { useCities } from '../../hooks/swrHooks'
import useSWR from 'swr'
import { fetcher } from '../../http/axiosInstance'
import { BASE_URL } from '../../constants'
import { Accordion, ActionIcon, Box, Button, Flex, Select as MantineSelect, MantineStyleProp, rem, Slider, useMantineColorScheme } from '@mantine/core'
import { IconApi, IconArrowBackUp, IconArrowsMove, IconCircle, IconExclamationCircle, IconLine, IconPlus, IconPoint, IconPolygon, IconRuler, IconTable, IconUpload } from '@tabler/icons-react'
import { Accordion, ActionIcon, Autocomplete, Box, Button, CloseButton, Flex, Grid, Select as MantineSelect, Text as MantineText, MantineStyleProp, rem, ScrollAreaAutosize, Slider, useMantineColorScheme, Divider, Portal } from '@mantine/core'
import { IconApi, IconArrowBackUp, IconArrowsMove, IconCircle, IconExclamationCircle, IconLine, IconPlus, IconPoint, IconPolygon, IconRuler, IconSettings, IconTable, IconUpload } from '@tabler/icons-react'
import { getGridCellPosition } from './mapUtils'
import { IFigure, ILine } from '../../interfaces/gis'
import { Height } from '@mui/icons-material'
import axios from 'axios'
import ObjectParameter from './ObjectParameter'
import { IObjectData, IObjectParam } from '../../interfaces/objects'
import ObjectData from './ObjectData'
const MapComponent = () => {
const { cities } = useCities(100, 1)
//const { cities } = useCities(100, 1)
useEffect(() => {
if (cities) {
cities.map((city: any) => {
citiesLayer.current?.getSource()?.addFeature(new Feature(new Point(fromLonLat([city.longitude, city.width]))))
})
}
}, [cities])
// useEffect(() => {
// if (cities) {
// cities.map((city: any) => {
// citiesLayer.current?.getSource()?.addFeature(new Feature(new Point(fromLonLat([city.longitude, city.width]))))
// })
// }
// }, [cities])
const [currentCoordinate, setCurrentCoordinate] = useState<Coordinate | null>(null)
const [currentZ, setCurrentZ] = useState<number | undefined>(undefined)
@ -104,6 +106,10 @@ const MapComponent = () => {
source: new VectorSource()
}))
const linesLayer = useRef<VectorLayer>(new VectorLayer({
source: new VectorSource()
}))
const regionsLayer = useRef<VectorImageLayer>(new VectorImageLayer({
source: regionsLayerSource,
style: regionsLayerStyle
@ -576,7 +582,7 @@ const MapComponent = () => {
map.current = new Map({
controls: [],
layers: [baseLayer.current, satLayer.current, regionsLayer.current, citiesLayer.current, figuresLayer.current, drawingLayer.current, imageLayer.current, overlayLayer.current, nodeLayer.current],
layers: [baseLayer.current, satLayer.current, regionsLayer.current, citiesLayer.current, linesLayer.current, figuresLayer.current, drawingLayer.current, imageLayer.current, overlayLayer.current, nodeLayer.current],
target: mapElement.current as HTMLDivElement,
view: new View({
center: transform([129.7659541, 62.009504], 'EPSG:4326', 'EPSG:3857'),//center: fromLonLat([130.401113, 67.797368]),
@ -596,6 +602,14 @@ const MapComponent = () => {
setCurrentY(tileY)
})
map.current.on('click', function (e: MapBrowserEvent<any>) {
const pixel = map.current?.getEventPixel(e.originalEvent)
map.current?.forEachFeatureAtPixel(pixel, function (feature) {
setCurrentObjectId(feature.get('object_id'))
})
})
const modify = new Modify({ source: drawingLayerSource.current })
map.current.addInteraction(modify)
@ -842,291 +856,407 @@ const MapComponent = () => {
];
}
const [currentObjectId, setCurrentObjectId] = useState<string | null>(null)
const [selectedCity, setSelectedCity] = useState<number | null>(null)
const [selectedYear, setSelectedYear] = useState<number | null>(2023)
const [citiesPage, setCitiesPage] = useState<number>(0)
const [searchCity, setSearchCity] = useState<string | undefined>("")
const { data: currentObjectData } = useSWR(
currentObjectId ? `/general/objects/${currentObjectId}` : null,
(url) => fetcher(url, BASE_URL.ems),
{
revalidateOnFocus: false
}
)
const { data: valuesData } = useSWR(
currentObjectId ? `/general/values/all?object_id=${currentObjectId}` : null,
(url) => fetcher(url, BASE_URL.ems),
{
revalidateOnFocus: false
}
)
const { data: citiesData } = useSWR(
`/general/cities/all?limit=${10}&offset=${citiesPage || 0}${searchCity ? `&search=${searchCity}` : ''}`,
(url) => fetcher(url, BASE_URL.ems),
{
revalidateOnFocus: false
}
)
const { data: figuresData, isValidating: figuresValidating } = useSWR(
selectedCity && selectedYear ? `/gis/figures/all?city_id=${selectedCity}&year=${selectedYear}&offset=0&limit=${10000}` : null,
(url) => axios.get(url, {
baseURL: BASE_URL.ems
}).then((res) => res.data),
{
revalidateOnFocus: false
}
)
const { data: linesData } = useSWR(
!figuresValidating && selectedCity && selectedYear ? `/gis/lines/all?city_id=${selectedCity}&year=${selectedYear}&offset=0&limit=${10000}` : null,
(url) => axios.get(url, {
baseURL: BASE_URL.ems
}).then((res) => {
return res.data
}),
{
revalidateOnFocus: false
}
)
useEffect(() => {
if (Array.isArray(figuresData)) {
figuresLayer.current.getSource()?.clear()
if (figuresData.length > 0) {
const scaling = {
w: 10000, // responseData[0].width
h: 10000 // responseData[0].width
}
figuresData.map((figure: IFigure) => {
if (figure.figure_type_id == 1) {
const width = figure.width * scaling.w
const height = figure.height * scaling.h
const left = figure.left * scaling.w
const top = figure.top * scaling.h
const centerX = mapCenter[0] + left + (width / 2)
const centerY = mapCenter[1] - top - (height / 2)
const radius = width / 2;
const circleGeom = new Circle([centerX, centerY], radius)
const ellipseGeom = fromCircle(circleGeom, 64)
ellipseGeom.scale(1, height / width)
const feature = new Feature(ellipseGeom)
feature.setStyle(firstStyleFunction(feature))
feature.set('object_id', figure.object_id)
figuresLayer.current?.getSource()?.addFeature(feature)
}
if (figure.figure_type_id == 3) {
const x = figure.left * scaling.w
const y = figure.top * scaling.h
const center = [mapCenter[0] + x, mapCenter[1] - y]
const coords = figure.points?.split(' ').map(pair => {
const [x, y] = pair.split(';').map(Number)
return [
center[0] + (x * scaling.w),
center[1] - (y * scaling.h)
]
})
if (coords) {
const polygon = new Polygon([coords])
const feature = new Feature({
geometry: polygon
})
feature.set('object_id', figure.object_id)
feature.setStyle(thirdStyleFunction(feature))
figuresLayer.current?.getSource()?.addFeature(feature)
}
}
if (figure.figure_type_id == 4) {
const width = figure.width * scaling.w
const height = figure.height * scaling.h
const left = figure.left * scaling.w
const top = figure.top * scaling.h
const halfWidth = width / 2
const halfHeight = height / 2
const center = [mapCenter[0] + left + halfWidth, mapCenter[1] - top - halfHeight]
const testCoords = [
[center[0] - halfWidth, center[1] - halfHeight],
[center[0] - halfWidth, center[1] + halfHeight],
[center[0] + halfWidth, center[1] + halfHeight],
[center[0] + halfWidth, center[1] - halfHeight],
[center[0] - halfWidth, center[1] - halfHeight]
]
const geometry1 = new Polygon([testCoords])
const anchor1 = center
geometry1.rotate(-figure.angle * Math.PI / 180, anchor1)
const feature1 = new Feature(geometry1)
feature1.set('object_id', figure.object_id)
feature1.set('angle', figure.angle)
feature1.setStyle(fourthStyleFunction(feature1))
figuresLayer.current?.getSource()?.addFeature(feature1)
}
})
}
}
if (Array.isArray(linesData)) {
linesLayer.current.getSource()?.clear()
if (linesData.length > 0) {
const scaling = {
w: 10000, // responseData[0].width
h: 10000 // responseData[0].width
}
linesData.map((line: ILine) => {
const x1 = line.x1 * scaling.w
const y1 = line.y1 * scaling.h
const x2 = line.x2 * scaling.w
const y2 = line.y2 * scaling.h
const center = [mapCenter[0], mapCenter[1]]
const testCoords = [
[center[0] + x1, center[1] - y1],
[center[0] + x2, center[1] - y2],
]
const feature = new Feature(new LineString(testCoords))
feature.setStyle(styleFunction(feature))
feature.set('object_id', line.object_id)
linesLayer.current?.getSource()?.addFeature(feature)
})
}
}
}, [figuresData, linesData, selectedCity, selectedYear])
return (
<Box w={'100%'} h={'calc(100% - 64px)'} mah={'100%'} flex={'1'} pos={'relative'}>
<ActionIcon.Group orientation='vertical' pos='absolute' top='8px' right='8px' style={{ zIndex: 1, backdropFilter: 'blur(8px)', backgroundColor: colorScheme === 'light' ? '#FFFFFFAA' : '#000000AA', borderRadius: '4px' }}>
<ActionIcon size='lg' variant='transparent' onClick={() => {
fetch(`${import.meta.env.VITE_API_EMS_URL}/hello`, { method: 'GET' }).then(res => console.log(res))
}}>
<IconApi />
</ActionIcon>
<Box w={'100%'} h='100%' pos={'relative'}>
<Portal target='#header-portal'>
<Flex gap={'sm'} direction={'row'}>
<form>
<Autocomplete
placeholder="Район"
flex={'1'}
data={citiesData ? citiesData.map((item: any) => ({ label: item.name, value: item.id.toString() })) : []}
//onSelect={(e) => console.log(e.currentTarget.value)}
onChange={(value) => setSearchCity(value)}
onOptionSubmit={(value) => setSelectedCity(Number(value))}
rightSection={
searchCity !== '' && (
<CloseButton
size="sm"
onMouseDown={(event) => event.preventDefault()}
onClick={() => {
setSearchCity('')
setSelectedCity(null)
}}
aria-label="Clear value"
/>
)
}
value={searchCity}
/>
</form>
<ActionIcon size='lg' variant='transparent' onClick={() => {
saveFeatures()
}}>
<IconExclamationCircle />
</ActionIcon>
<MantineSelect
data={[
{ label: '2018', value: '2018' },
{ label: '2019', value: '2019' },
{ label: '2020', value: '2020' },
{ label: '2021', value: '2021' },
{ label: '2022', value: '2022' },
{ label: '2023', value: '2023' },
{ label: '2024', value: '2024' },
]}
onChange={(e) => {
setSelectedYear(Number(e))
}}
defaultValue={selectedYear?.toString()}
//variant="unstyled"
allowDeselect={false}
/>
<ActionIcon size='lg' variant='transparent' onClick={() => {
draw.current?.removeLastPoint()
}}>
<IconArrowBackUp />
</ActionIcon>
<ActionIcon
size='lg'
variant={currentTool === 'Point' ? 'filled' : 'transparent'}
onClick={() => {
handleToolSelect('Point')
}}>
<IconPoint />
</ActionIcon>
<ActionIcon
size='lg'
variant={currentTool === 'LineString' ? 'filled' : 'transparent'}
onClick={() => {
handleToolSelect('LineString')
}}>
<IconLine />
</ActionIcon>
<ActionIcon
size='lg'
variant={currentTool === 'Polygon' ? 'filled' : 'transparent'}
onClick={() => {
handleToolSelect('Polygon')
}}>
<IconPolygon />
</ActionIcon>
<ActionIcon
size='lg'
variant={currentTool === 'Circle' ? 'filled' : 'transparent'}
onClick={() => {
handleToolSelect('Circle')
}}>
<IconCircle />
</ActionIcon>
<ActionIcon
size='lg'
variant='transparent'
onClick={() => map?.current?.addInteraction(new Translate())}
>
<IconArrowsMove />
</ActionIcon>
<ActionIcon
size='lg'
variant='transparent'
>
<IconRuler />
</ActionIcon>
</ActionIcon.Group>
<Flex direction='column' style={{
...mapControlsStyle,
maxWidth: '300px',
width: '100%',
top: '8px',
left: '8px'
}}>
<Flex direction='row'>
<ActionIcon
size='lg'
variant='transparent'
onClick={() => submitOverlay()}
title='Настройки ИКС'
>
<IconUpload style={{ width: rem(20), height: rem(20) }} />
<IconSettings style={{ width: rem(20), height: rem(20) }} />
</ActionIcon>
</Flex>
</Portal>
<Flex w={'100%'} h={'100%'} pos={'absolute'}>
<ActionIcon.Group orientation='vertical' pos='absolute' top='8px' right='8px' style={{ zIndex: 1, backdropFilter: 'blur(8px)', backgroundColor: colorScheme === 'light' ? '#FFFFFFAA' : '#000000AA', borderRadius: '4px' }}>
<ActionIcon size='lg' variant='transparent' onClick={() => {
fetch(`${import.meta.env.VITE_API_EMS_URL}/hello`, { method: 'GET' }).then(res => console.log(res))
}}>
<IconApi />
</ActionIcon>
<ActionIcon size='lg' variant='transparent' onClick={() => {
saveFeatures()
}}>
<IconExclamationCircle />
</ActionIcon>
<ActionIcon size='lg' variant='transparent' onClick={() => {
draw.current?.removeLastPoint()
}}>
<IconArrowBackUp />
</ActionIcon>
<ActionIcon
size='lg'
variant={currentTool === 'Point' ? 'filled' : 'transparent'}
onClick={() => {
handleToolSelect('Point')
}}>
<IconPoint />
</ActionIcon>
<ActionIcon
size='lg'
variant={currentTool === 'LineString' ? 'filled' : 'transparent'}
onClick={() => {
handleToolSelect('LineString')
}}>
<IconLine />
</ActionIcon>
<ActionIcon
size='lg'
variant={currentTool === 'Polygon' ? 'filled' : 'transparent'}
onClick={() => {
handleToolSelect('Polygon')
}}>
<IconPolygon />
</ActionIcon>
<ActionIcon
size='lg'
variant={currentTool === 'Circle' ? 'filled' : 'transparent'}
onClick={() => {
handleToolSelect('Circle')
}}>
<IconCircle />
</ActionIcon>
<ActionIcon
size='lg'
variant='transparent'
title='Добавить подложку'
onClick={() => map?.current?.addInteraction(new Translate())}
>
<IconPlus style={{ width: rem(20), height: rem(20) }} />
<IconArrowsMove />
</ActionIcon>
</Flex>
<Flex align='center' direction='row' p='sm' gap='sm'>
<Slider w='100%' min={0} max={1} step={0.001} value={satelliteOpacity} defaultValue={satelliteOpacity} onChange={(value) => setSatelliteOpacity(Array.isArray(value) ? value[0] : value)} />
<ActionIcon
size='lg'
variant='transparent'
>
<IconRuler />
</ActionIcon>
</ActionIcon.Group>
<MantineSelect variant='filled' value={satMapsProvider} data={[{ label: 'Google', value: 'google' }, { label: 'Yandex', value: 'yandex' }, { label: 'Custom', value: 'custom' }]} onChange={(value) => setSatMapsProvider(value as SatelliteMapsProvider)} />
</Flex>
<Accordion variant='filled' style={{ backgroundColor: 'transparent' }} defaultValue='Объекты'>
<Accordion.Item key={'s'} value={'Объекты'}>
<Accordion.Control icon={<IconTable />}>{'Объекты'}</Accordion.Control>
<Accordion.Panel>
<Button
onClick={async () => {
//open()
const city_id = 145
const year = 2023
const figuresLimit = 10000
const linesLimit = 10000
figuresLayer.current.getSource()?.clear()
try {
// const response = await fetch(`${import.meta.env.VITE_API_EMS_URL}/gis/images/all?city_id=${city_id}`, {
// method: 'GET',
// headers: {
// 'Content-Type': 'application/json',
// },
// })
//const responseData = await response.json()
const scaling = {
w: 10000, // responseData[0].width
h: 10000 // responseData[0].width
}
const figuresResponse = await fetch(`${import.meta.env.VITE_API_EMS_URL}/gis/figures/all?city_id=${city_id}&year=${year}&offset=0&limit=${figuresLimit}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
const linesResponse = await fetch(`${import.meta.env.VITE_API_EMS_URL}/gis/lines/all?city_id=${city_id}&year=${year}&offset=0&limit=${linesLimit}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
await linesResponse.json().then(linesData => {
linesData.map((line: ILine) => {
const x1 = line.x1 * scaling.w
const y1 = line.y1 * scaling.h
const x2 = line.x2 * scaling.w
const y2 = line.y2 * scaling.h
const center = [mapCenter[0], mapCenter[1]]
const testCoords = [
[center[0] + x1, center[1] - y1],
[center[0] + x2, center[1] - y2],
]
const feature = new Feature(new LineString(testCoords))
feature.setStyle(styleFunction(feature))
feature.set('object_id', line.object_id)
figuresLayer.current?.getSource()?.addFeature(feature)
})
})
await figuresResponse.json().then(figuresData => {
figuresData.map((figure: IFigure) => {
if (figure.figure_type_id == 1) {
const width = figure.width * scaling.w
const height = figure.height * scaling.h
const left = figure.left * scaling.w
const top = figure.top * scaling.h
const centerX = mapCenter[0] + left + (width / 2)
const centerY = mapCenter[1] - top - (height / 2)
const radius = width / 2;
const circleGeom = new Circle([centerX, centerY], radius)
const ellipseGeom = fromCircle(circleGeom, 64)
ellipseGeom.scale(1, height / width)
const feature = new Feature(ellipseGeom)
feature.setStyle(firstStyleFunction(feature))
feature.set('object_id', figure.object_id)
figuresLayer.current?.getSource()?.addFeature(feature)
}
if (figure.figure_type_id == 3) {
const x = figure.left * scaling.w
const y = figure.top * scaling.h
const center = [mapCenter[0] + x, mapCenter[1] - y]
const coords = figure.points?.split(' ').map(pair => {
const [x, y] = pair.split(';').map(Number)
return [
center[0] + (x * scaling.w),
center[1] - (y * scaling.h)
]
})
if (coords) {
const polygon = new Polygon([coords])
const feature = new Feature({
geometry: polygon
})
feature.set('object_id', figure.object_id)
feature.setStyle(thirdStyleFunction(feature))
figuresLayer.current?.getSource()?.addFeature(feature)
}
}
if (figure.figure_type_id == 4) {
const width = figure.width * scaling.w
const height = figure.height * scaling.h
const left = figure.left * scaling.w
const top = figure.top * scaling.h
const halfWidth = width / 2
const halfHeight = height / 2
const center = [mapCenter[0] + left + halfWidth, mapCenter[1] - top - halfHeight]
const testCoords = [
[center[0] - halfWidth, center[1] - halfHeight],
[center[0] - halfWidth, center[1] + halfHeight],
[center[0] + halfWidth, center[1] + halfHeight],
[center[0] + halfWidth, center[1] - halfHeight],
[center[0] - halfWidth, center[1] - halfHeight]
]
const geometry1 = new Polygon([testCoords])
const anchor1 = center
geometry1.rotate(-figure.angle * Math.PI / 180, anchor1)
const feature1 = new Feature(geometry1)
feature1.set('object_id', figure.object_id)
feature1.set('angle', figure.angle)
feature1.setStyle(fourthStyleFunction(feature1))
figuresLayer.current?.getSource()?.addFeature(feature1)
}
})
})
} catch (err) {
console.error('No data')
}
}}
<Flex direction='column' mah={'86%'} pl='sm' style={{
...mapControlsStyle,
maxWidth: '300px',
width: '100%',
top: '8px',
left: '8px',
}}>
<ScrollAreaAutosize offsetScrollbars>
<Flex direction='row'>
<ActionIcon
size='lg'
variant='transparent'
onClick={() => submitOverlay()}
>
Test
</Button>
</Accordion.Panel>
</Accordion.Item>
</Accordion>
</Flex>
<IconUpload style={{ width: rem(20), height: rem(20) }} />
</ActionIcon>
<Flex direction='row' pos='absolute' bottom='8px' left='8px' style={{ ...mapControlsStyle }}>
<Stack>
<Typography>
<ActionIcon
size='lg'
variant='transparent'
title='Добавить подложку'
>
<IconPlus style={{ width: rem(20), height: rem(20) }} />
</ActionIcon>
</Flex>
<Flex align='center' direction='row' p='sm' gap='sm'>
<Slider w='100%' min={0} max={1} step={0.001} value={satelliteOpacity} defaultValue={satelliteOpacity} onChange={(value) => setSatelliteOpacity(Array.isArray(value) ? value[0] : value)} />
<MantineSelect variant='filled' value={satMapsProvider} data={[{ label: 'Google', value: 'google' }, { label: 'Yandex', value: 'yandex' }, { label: 'Custom', value: 'custom' }]} onChange={(value) => setSatMapsProvider(value as SatelliteMapsProvider)} />
</Flex>
<Accordion variant='filled' style={{ backgroundColor: 'transparent' }} defaultValue='Объекты'>
<Accordion.Item key={'objects'} value={'Объекты'}>
<Accordion.Control icon={<IconTable />}>{'Объекты'}</Accordion.Control>
<Accordion.Panel>
</Accordion.Panel>
</Accordion.Item>
<Accordion.Item key={'current_object'} value='current_object'>
<Accordion.Control icon={<IconTable />}>
{'Текущий объект'}
</Accordion.Control>
<Accordion.Panel>
<ObjectData {...currentObjectData as IObjectData} />
</Accordion.Panel>
</Accordion.Item>
{valuesData && <Accordion.Item key={'parameters'} value={'Параметры объекта'}>
<Accordion.Control icon={<IconTable />}>{'Параметры объекта'}</Accordion.Control>
<Accordion.Panel>
<Flex gap={'sm'} direction={'column'}>
{Array.isArray(valuesData) && valuesData.map((param: IObjectParam) => {
return (
<ObjectParameter key={param.id_param} value={param.value} id_param={param.id_param} />
)
})}
</Flex>
</Accordion.Panel>
</Accordion.Item>}
</Accordion>
</ScrollAreaAutosize>
</Flex>
<Flex gap='sm' p={'4px'} miw={'100%'} fz={'xs'} pos='absolute' bottom='0px' left='0px' style={{ ...mapControlsStyle, borderRadius: 0 }}>
<MantineText fz='xs' w={rem(130)}>
x: {currentCoordinate?.[0]}
</Typography>
<Typography>
</MantineText>
<MantineText fz='xs' w={rem(130)}>
y: {currentCoordinate?.[1]}
</Typography>
</Stack>
</MantineText>
<Typography>
Z={currentZ}
X={currentX}
Y={currentY}
</Typography>
<Divider orientation='vertical' />
<MantineText fz='xs'>
Z={currentZ}
</MantineText>
<MantineText fz='xs'>
X={currentX}
</MantineText>
<MantineText fz='xs'>
Y={currentY}
</MantineText>
<MantineText fz='xs' ml='auto'>
{statusText}
</MantineText>
</Flex>
</Flex>
<Flex direction='row' style={{ ...mapControlsStyle, bottom: '8px', right: '8px' }}>
{statusText}
</Flex>
<div
id="map-container"