|
|
|
|
@ -1,154 +1,275 @@
|
|
|
|
|
import { Paper, Flex, ScrollArea, Autocomplete } from '@mantine/core';
|
|
|
|
|
import { ArticleCardVertical } from './ArticleCard';
|
|
|
|
|
import Map, { Source, Layer, ScaleControl } from 'react-map-gl/maplibre';
|
|
|
|
|
import { useState, useEffect, useRef } from 'react';
|
|
|
|
|
import Fuse from 'fuse.js'
|
|
|
|
|
import { Paper, Flex, ScrollArea, Autocomplete, Title } from "@mantine/core";
|
|
|
|
|
import { ArticleCardVertical } from "./ArticleCard";
|
|
|
|
|
import Map, {
|
|
|
|
|
Source,
|
|
|
|
|
Layer,
|
|
|
|
|
ScaleControl,
|
|
|
|
|
GeolocateControl,
|
|
|
|
|
} from "react-map-gl/maplibre";
|
|
|
|
|
import { useState, useEffect, useRef } from "react";
|
|
|
|
|
import Fuse from "fuse.js";
|
|
|
|
|
|
|
|
|
|
// import mapstyle from './assets/basemap.json';
|
|
|
|
|
import mapstyle from './assets/basemap-mapbox.json';
|
|
|
|
|
import pin from './assets/pin.png'
|
|
|
|
|
import plus from './assets/plus.png'
|
|
|
|
|
import minus from './assets/minus.png'
|
|
|
|
|
import mapstyle from "./assets/basemap-mapbox.json";
|
|
|
|
|
import plus from "./assets/plus.png";
|
|
|
|
|
import minus from "./assets/minus.png";
|
|
|
|
|
|
|
|
|
|
import pin from "./assets/pin.png";
|
|
|
|
|
import prishvin from "./assets/prishvin.png";
|
|
|
|
|
import ostrovskiy from "./assets/ostrovskiy.png";
|
|
|
|
|
import brusov from "./assets/brusov.png";
|
|
|
|
|
|
|
|
|
|
export function KartaPage() {
|
|
|
|
|
const mapRef = useRef(null);
|
|
|
|
|
const [initial, setInitial] = useState(null);
|
|
|
|
|
const [articles, setArticles] = useState(null);
|
|
|
|
|
const [search, setSearch] = useState('');
|
|
|
|
|
const [search, setSearch] = useState("");
|
|
|
|
|
const [selected, setSelected] = useState(-1);
|
|
|
|
|
const [cursor, setCursor] = useState('grab');
|
|
|
|
|
const [cursor, setCursor] = useState("grab");
|
|
|
|
|
|
|
|
|
|
const host = "http://strapi.litmusmap.ru"
|
|
|
|
|
const host = "http://strapi.litmusmap.ru";
|
|
|
|
|
|
|
|
|
|
// Load icons
|
|
|
|
|
const handleMapLoad = (e) => {
|
|
|
|
|
if (mapRef.current) {
|
|
|
|
|
const pinImage = new Image();
|
|
|
|
|
pinImage.onload = () => {
|
|
|
|
|
if (!mapRef.current.hasImage('pin')) {
|
|
|
|
|
mapRef.current.addImage('pin-marker', pinImage);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
pinImage.src = pin;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const icons = ["pin", "prishvin", "ostrovskiy", "brusov"];
|
|
|
|
|
const pinImage = new Image();
|
|
|
|
|
pinImage.src = pin;
|
|
|
|
|
pinImage.onload = () => mapRef.current.addImage("pin-marker", pinImage);
|
|
|
|
|
const prishvinImage = new Image();
|
|
|
|
|
prishvinImage.src = prishvin;
|
|
|
|
|
prishvinImage.onload = () =>
|
|
|
|
|
mapRef.current.addImage("prishvin-marker", prishvinImage);
|
|
|
|
|
const ostrovskiyImage = new Image();
|
|
|
|
|
ostrovskiyImage.src = ostrovskiy;
|
|
|
|
|
ostrovskiyImage.onload = () =>
|
|
|
|
|
mapRef.current.addImage("ostrovskiy-marker", ostrovskiyImage);
|
|
|
|
|
const brusovImage = new Image();
|
|
|
|
|
brusovImage.src = brusov;
|
|
|
|
|
brusovImage.onload = () =>
|
|
|
|
|
mapRef.current.addImage("brusov-marker", brusovImage);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Load articles
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
fetch(`${host}/api/articles?populate=*`)
|
|
|
|
|
.then(r => r.json())
|
|
|
|
|
.then(d => {
|
|
|
|
|
setInitial(d.data)
|
|
|
|
|
setArticles(d.data)
|
|
|
|
|
})
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
.then((r) => r.json())
|
|
|
|
|
.then((d) => {
|
|
|
|
|
setInitial(d.data);
|
|
|
|
|
setArticles(d.data);
|
|
|
|
|
});
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// Search
|
|
|
|
|
const fuse = initial !== null && new Fuse(initial, {
|
|
|
|
|
keys: [
|
|
|
|
|
"attributes.tags.value"
|
|
|
|
|
]
|
|
|
|
|
});
|
|
|
|
|
const fuse =
|
|
|
|
|
initial !== null &&
|
|
|
|
|
new Fuse(initial, {
|
|
|
|
|
keys: ["attributes.tags.value"],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const foundArticles = fuse && fuse.search(search).map(e => e.item)
|
|
|
|
|
const updatedArticles = search.length > 0 ? foundArticles : initial
|
|
|
|
|
setArticles(updatedArticles)
|
|
|
|
|
}, [search])
|
|
|
|
|
const foundArticles = fuse && fuse.search(search).map((e) => e.item);
|
|
|
|
|
const updatedArticles = search.length > 0 ? foundArticles : initial;
|
|
|
|
|
setArticles(updatedArticles);
|
|
|
|
|
}, [search]);
|
|
|
|
|
|
|
|
|
|
// Select article interaction
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (selected > 0) {
|
|
|
|
|
const selectedArticleIndex = articles.findIndex((a) => a.id == selected);
|
|
|
|
|
const selectedArticle = articles[selectedArticleIndex];
|
|
|
|
|
const reorderedArticles = [
|
|
|
|
|
selectedArticle,
|
|
|
|
|
...articles.filter((a) => a.id !== selected),
|
|
|
|
|
];
|
|
|
|
|
setArticles(reorderedArticles);
|
|
|
|
|
mapRef.current?.flyTo({
|
|
|
|
|
center: [
|
|
|
|
|
selectedArticle.attributes.longitude,
|
|
|
|
|
selectedArticle.attributes.latitude,
|
|
|
|
|
],
|
|
|
|
|
zoom: 10,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}, [selected]);
|
|
|
|
|
|
|
|
|
|
// List-map interaction
|
|
|
|
|
const handleAddressClick = (id) => {
|
|
|
|
|
setSelected(id)
|
|
|
|
|
const { longitude, latitude } = articles.find(e => e.id == id).attributes
|
|
|
|
|
id > 0 && articles && mapRef.current?.flyTo({ center: [longitude, latitude] })
|
|
|
|
|
}
|
|
|
|
|
setSelected(id);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Zoom
|
|
|
|
|
const ZoomControl = () => {
|
|
|
|
|
const ZoomControl = (props) => {
|
|
|
|
|
const zoomIn = () => {
|
|
|
|
|
mapRef.current.zoomTo(mapRef.current.getZoom() + 1)
|
|
|
|
|
}
|
|
|
|
|
mapRef.current.zoomTo(mapRef.current.getZoom() + 1);
|
|
|
|
|
};
|
|
|
|
|
const zoomOut = () => {
|
|
|
|
|
mapRef.current.zoomTo(mapRef.current.getZoom() - 1)
|
|
|
|
|
}
|
|
|
|
|
mapRef.current.zoomTo(mapRef.current.getZoom() - 1);
|
|
|
|
|
};
|
|
|
|
|
return (
|
|
|
|
|
<Flex style={{
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
top: '1rem',
|
|
|
|
|
left: '1rem',
|
|
|
|
|
}}>
|
|
|
|
|
<Paper onClick={zoomIn} bg={'none'} sx={{"&:hover": {backgroundColor: "white", cursor: 'pointer'}}} maw={48} mah={48}><img src={plus} style={{width: '48px', height: '48px'}}></img></Paper>
|
|
|
|
|
<Paper onClick={zoomOut} bg={'none'} sx={{"&:hover": {backgroundColor: "white", cursor: 'pointer'}}} maw={48} mah={48}><img src={minus} style={{width: '48px', height: '48px'}}></img></Paper>
|
|
|
|
|
<Flex
|
|
|
|
|
style={{
|
|
|
|
|
position: "absolute",
|
|
|
|
|
bottom: props.bottom,
|
|
|
|
|
right: props.right,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Paper
|
|
|
|
|
onClick={zoomOut}
|
|
|
|
|
radius="xl"
|
|
|
|
|
bg={"none"}
|
|
|
|
|
sx={{ "&:hover": { backgroundColor: "white", cursor: "pointer" } }}
|
|
|
|
|
maw={48}
|
|
|
|
|
mah={48}
|
|
|
|
|
>
|
|
|
|
|
<img src={minus} style={{ width: "48px", height: "48px" }}></img>
|
|
|
|
|
</Paper>
|
|
|
|
|
<Paper
|
|
|
|
|
onClick={zoomIn}
|
|
|
|
|
radius="xl"
|
|
|
|
|
bg={"none"}
|
|
|
|
|
sx={{ "&:hover": { backgroundColor: "white", cursor: "pointer" } }}
|
|
|
|
|
maw={48}
|
|
|
|
|
mah={48}
|
|
|
|
|
>
|
|
|
|
|
<img src={plus} style={{ width: "48px", height: "48px" }}></img>
|
|
|
|
|
</Paper>
|
|
|
|
|
</Flex>
|
|
|
|
|
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div style={{ position: 'relative' }}>
|
|
|
|
|
<div style={{ position: "relative" }}>
|
|
|
|
|
<Map
|
|
|
|
|
ref={mapRef}
|
|
|
|
|
initialViewState={{
|
|
|
|
|
longitude: 60,
|
|
|
|
|
longitude: 30,
|
|
|
|
|
latitude: 60,
|
|
|
|
|
zoom: 4
|
|
|
|
|
zoom: 4,
|
|
|
|
|
}}
|
|
|
|
|
minZoom={3.5}
|
|
|
|
|
maxZoom={16}
|
|
|
|
|
maxBounds={[
|
|
|
|
|
[-20, 35],
|
|
|
|
|
[200, 80],
|
|
|
|
|
]}
|
|
|
|
|
style={{
|
|
|
|
|
height: '90vh',
|
|
|
|
|
height: "90vh",
|
|
|
|
|
}}
|
|
|
|
|
cursor={cursor}
|
|
|
|
|
mapStyle={mapstyle}
|
|
|
|
|
interactiveLayerIds={['points-layer']}
|
|
|
|
|
onClick={e => { e.features[0] ? setSelected(e.features[0].properties.id) : setSelected(-1) }}
|
|
|
|
|
onMouseEnter={() => setCursor('pointer')}
|
|
|
|
|
onMouseLeave={() => setCursor('grab')}
|
|
|
|
|
interactiveLayerIds={["points-layer"]}
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
// console.log(e.features);
|
|
|
|
|
e.features[0]
|
|
|
|
|
? (setSelected(e.features[0].properties.id),
|
|
|
|
|
mapRef.current.flyTo({
|
|
|
|
|
center: e.features[0].geometry.coordinates,
|
|
|
|
|
zoom: mapRef.current.getZoom() + 6,
|
|
|
|
|
}))
|
|
|
|
|
: setSelected(-1);
|
|
|
|
|
}}
|
|
|
|
|
onMouseEnter={() => setCursor("pointer")}
|
|
|
|
|
onMouseLeave={() => setCursor("grab")}
|
|
|
|
|
onLoad={handleMapLoad}
|
|
|
|
|
>
|
|
|
|
|
<Source
|
|
|
|
|
id='points'
|
|
|
|
|
type='vector'
|
|
|
|
|
id="points"
|
|
|
|
|
type="vector"
|
|
|
|
|
tiles={[`${host}/tiles/{z}/{x}/{y}.pbf`]}
|
|
|
|
|
maxzoom={10}
|
|
|
|
|
>
|
|
|
|
|
<Layer
|
|
|
|
|
id='points-layer'
|
|
|
|
|
type='circle'
|
|
|
|
|
source-layer='layer'
|
|
|
|
|
filter={articles === null || articles.length == 0 ? false : ['in', 'id', ...articles.map(a => a.id)]}
|
|
|
|
|
id="points-layer"
|
|
|
|
|
type="circle"
|
|
|
|
|
source-layer="layer"
|
|
|
|
|
filter={
|
|
|
|
|
articles === null || articles.length == 0
|
|
|
|
|
? false
|
|
|
|
|
: ["in", "id", ...articles.map((a) => a.id)]
|
|
|
|
|
}
|
|
|
|
|
paint={{
|
|
|
|
|
"circle-color": ['match', ['get', 'id'], selected, '#FF0000', '#1c9099'],
|
|
|
|
|
"circle-color": [
|
|
|
|
|
"match",
|
|
|
|
|
["get", "id"],
|
|
|
|
|
selected,
|
|
|
|
|
"#e66a5a",
|
|
|
|
|
"#F2994A",
|
|
|
|
|
],
|
|
|
|
|
"circle-radius": 8,
|
|
|
|
|
"circle-opacity": 0.7,
|
|
|
|
|
"circle-opacity": ["step", ["zoom"], 0.8, 7, 0],
|
|
|
|
|
}}
|
|
|
|
|
maxzoom={7}
|
|
|
|
|
/>
|
|
|
|
|
<Layer
|
|
|
|
|
id='points2-layer'
|
|
|
|
|
type='symbol'
|
|
|
|
|
source-layer='layer'
|
|
|
|
|
filter={articles === null || articles.length == 0 ? false : ['in', 'id', ...articles.map(a => a.id)]}
|
|
|
|
|
id="icons-layer"
|
|
|
|
|
type="symbol"
|
|
|
|
|
source-layer="layer"
|
|
|
|
|
filter={
|
|
|
|
|
articles === null || articles.length == 0
|
|
|
|
|
? false
|
|
|
|
|
: ["in", "id", ...articles.map((a) => a.id)]
|
|
|
|
|
}
|
|
|
|
|
layout={{
|
|
|
|
|
'icon-image': 'pin-marker',
|
|
|
|
|
'icon-size': 1
|
|
|
|
|
"icon-image": "pin-marker",
|
|
|
|
|
"icon-size": 1,
|
|
|
|
|
}}
|
|
|
|
|
minzoom={7}
|
|
|
|
|
maxzoom={14}
|
|
|
|
|
/>
|
|
|
|
|
<Layer
|
|
|
|
|
id="portret-layer"
|
|
|
|
|
type="symbol"
|
|
|
|
|
source-layer="layer"
|
|
|
|
|
filter={
|
|
|
|
|
articles === null || articles.length == 0
|
|
|
|
|
? false
|
|
|
|
|
: ["in", "id", ...articles.map((a) => a.id)]
|
|
|
|
|
}
|
|
|
|
|
layout={{
|
|
|
|
|
"icon-image": [
|
|
|
|
|
"match",
|
|
|
|
|
["get", "id"],
|
|
|
|
|
6,
|
|
|
|
|
"ostrovskiy-marker",
|
|
|
|
|
7,
|
|
|
|
|
"brusov-marker",
|
|
|
|
|
1,
|
|
|
|
|
"prishvin-marker",
|
|
|
|
|
"pin-marker",
|
|
|
|
|
],
|
|
|
|
|
"icon-size": 0.1,
|
|
|
|
|
}}
|
|
|
|
|
minzoom={14}
|
|
|
|
|
/>
|
|
|
|
|
</Source>
|
|
|
|
|
<ScaleControl />
|
|
|
|
|
<ZoomControl />
|
|
|
|
|
<ScaleControl position="bottom-right" />
|
|
|
|
|
<ZoomControl bottom="35px" right="4px" />
|
|
|
|
|
<GeolocateControl
|
|
|
|
|
showAccuracyCircle={false}
|
|
|
|
|
position="bottom-right"
|
|
|
|
|
style={{
|
|
|
|
|
marginBottom: "55px",
|
|
|
|
|
marginRight: "14px",
|
|
|
|
|
background: "none",
|
|
|
|
|
boxShadow: "none",
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</Map>
|
|
|
|
|
|
|
|
|
|
<Paper shadow={'md'} withBorder style={{
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
top: '1rem',
|
|
|
|
|
right: '1rem',
|
|
|
|
|
width: '36rem',
|
|
|
|
|
height: '83vh',
|
|
|
|
|
opacity: '90%',
|
|
|
|
|
padding: '1rem 0 1rem 1rem'
|
|
|
|
|
}}>
|
|
|
|
|
|
|
|
|
|
<Paper
|
|
|
|
|
shadow={"md"}
|
|
|
|
|
withBorder
|
|
|
|
|
style={{
|
|
|
|
|
position: "absolute",
|
|
|
|
|
top: "1rem",
|
|
|
|
|
left: "1rem",
|
|
|
|
|
width: "36rem",
|
|
|
|
|
height: "83vh",
|
|
|
|
|
opacity: "95%",
|
|
|
|
|
padding: "1rem 0 1rem 1rem",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Title order={3} mb={10}>
|
|
|
|
|
Статьи
|
|
|
|
|
</Title>
|
|
|
|
|
<Autocomplete
|
|
|
|
|
mr={20}
|
|
|
|
|
mb={30}
|
|
|
|
|
@ -158,7 +279,7 @@ export function KartaPage() {
|
|
|
|
|
onChange={setSearch}
|
|
|
|
|
data={[]}
|
|
|
|
|
/>
|
|
|
|
|
<ScrollArea h={'73vh'} type="auto">
|
|
|
|
|
<ScrollArea h={"67vh"} type="auto">
|
|
|
|
|
<Flex
|
|
|
|
|
mih={50}
|
|
|
|
|
gap="md"
|
|
|
|
|
@ -168,26 +289,29 @@ export function KartaPage() {
|
|
|
|
|
wrap="wrap"
|
|
|
|
|
mr={20}
|
|
|
|
|
>
|
|
|
|
|
{articles !== null && articles.length > 0 && articles.map(article => {
|
|
|
|
|
const articleInfo = {
|
|
|
|
|
"id": article.id,
|
|
|
|
|
"image": article.attributes.cover.data !== null ? host + article.attributes.cover.data.attributes.url : "",
|
|
|
|
|
"category": "Памятные места",
|
|
|
|
|
"title": article.attributes.title,
|
|
|
|
|
"address": article.attributes.address,
|
|
|
|
|
"coordinates": article.attributes.coordinates,
|
|
|
|
|
"selected": selected,
|
|
|
|
|
"handleAddressClick": handleAddressClick
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return <ArticleCardVertical key={article.id} {...articleInfo} />
|
|
|
|
|
}
|
|
|
|
|
)}
|
|
|
|
|
{articles !== null &&
|
|
|
|
|
articles.length > 0 &&
|
|
|
|
|
articles.map((article) => (
|
|
|
|
|
<ArticleCardVertical
|
|
|
|
|
key={article.id}
|
|
|
|
|
id={article.id}
|
|
|
|
|
image={
|
|
|
|
|
article.attributes.cover.data !== null
|
|
|
|
|
? host + article.attributes.cover.data.attributes.url
|
|
|
|
|
: ""
|
|
|
|
|
}
|
|
|
|
|
category="музеи"
|
|
|
|
|
title={article.attributes.title}
|
|
|
|
|
address={article.attributes.address}
|
|
|
|
|
longitude={article.attributes.longitude}
|
|
|
|
|
latitude={article.attributes.latitude}
|
|
|
|
|
selected={selected}
|
|
|
|
|
clickAddressAction={handleAddressClick}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</Flex>
|
|
|
|
|
</ScrollArea>
|
|
|
|
|
</Paper>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|