You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

431 lines
13 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import { Paper, Flex, ScrollArea, Autocomplete, Title } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { HeaderSimple } from "./Header";
import { FooterLinks } from "./Footer";
import headerLinks from "./assets/header.json";
import footerLinks from "./assets/footer.json";
import { ArticleCardVertical } from "./ArticleCard";
import { ArticleCardMobile } from "./ArticleCardMobile";
import Map, {
Source,
Layer,
ScaleControl,
GeolocateControl,
Popup,
} from "react-map-gl/maplibre";
import "maplibre-gl/dist/maplibre-gl.css";
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 plus from "./assets/plus.png";
import minus from "./assets/minus.png";
import pin from "./assets/pin.png";
export function KartaPage() {
const mapRef = useRef(null);
const [initial, setInitial] = useState(null);
const [articles, setArticles] = useState(null);
const [search, setSearch] = useState("");
const [selected, setSelected] = useState(-1);
const [cursor, setCursor] = useState("grab");
const [popupInfo, setPopupInfo] = useState(null);
const [openedMenu, menuactions] = useDisclosure(false);
const host = "https://strapi.litmusmap.ru";
// Load icons
const handleMapLoad = (e) => {
const pinImage = new Image();
pinImage.src = pin;
pinImage.onload = () => mapRef.current.addImage("pin-marker", pinImage);
const markers = initial.map(article => ({markerName: `marker-${article.id}`, markerUrl: `${host}${article.attributes.marker.data.attributes.formats.thumbnail.url}`}));
markers.map((m) => {
const markerImage = new Image();
markerImage.crossOrigin = "Anonymous"; // https://stackoverflow.com/questions/22097747/how-to-fix-getimagedata-error-the-canvas-has-been-tainted-by-cross-origin-data
markerImage.src = m.markerUrl;
markerImage.onload = () =>
mapRef.current.addImage(m.markerName, markerImage);
});
};
// Load articles
useEffect(() => {
fetch(`${host}/api/articles?populate=*`)
.then((r) => r.json())
.then((d) => {
setInitial(d.data);
setArticles(d.data);
});
}, []);
// Search
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]);
// 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,
});
} else {
mapRef.current?.flyTo({
center: [35, 57],
zoom: 5,
});
}
}, [selected]);
const handleAddressClick = (id) => {
setSelected(id);
};
// Zoom
const ZoomControl = (props) => {
const zoomIn = () => {
mapRef.current.zoomTo(mapRef.current.getZoom() + 1);
};
const zoomOut = () => {
mapRef.current.zoomTo(mapRef.current.getZoom() - 1);
};
return (
<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 (
<>
<HeaderSimple links={headerLinks.links} openedMenuKarta={openedMenu} menuactionskarta={menuactions}/>
<div style={{ position: "relative" }}>
<Map
ref={mapRef}
initialViewState={{
longitude: 35,
latitude: 60,
zoom: 4,
}}
minZoom={3.5}
maxZoom={16}
maxBounds={[
[-20, 35],
[200, 80],
]}
style={{
height: "90vh",
}}
cursor={cursor}
mapStyle={mapstyle}
interactiveLayerIds={["points-layer"]}
onClick={(e) => {
// console.log(e.features);
if (e.features[0]) {
setSelected(e.features[0].properties.id);
if (mapRef.current.getZoom() < 14) {
mapRef.current.flyTo({
center: e.features[0].geometry.coordinates,
zoom: mapRef.current.getZoom() + 6,
});
} else {
setPopupInfo({
longitude: e.features[0].geometry.coordinates[0],
latitude: e.features[0].geometry.coordinates[1],
id: e.features[0].properties.id,
});
}
} else {
setSelected(-1);
}
}}
onMouseEnter={() => setCursor("pointer")}
onMouseLeave={() => setCursor("grab")}
onLoad={handleMapLoad}
>
<Source
id="points"
type="vector"
tiles={[`${host}/tiles/{z}/{x}/{y}.pbf`]}
maxzoom={10}
>
{popupInfo && (
<Popup
key={popupInfo.id}
longitude={popupInfo.longitude}
latitude={popupInfo.latitude}
anchor="top"
closeButton={false}
onClose={() => setPopupInfo(null)}
>
<a href={`/article/${popupInfo.id}`}>подробнее</a>
</Popup>
)}
<Layer
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,
"#e66a5a",
"#F2994A",
],
"circle-radius": ["step", ["zoom"], 8, 7, 20, 12, 40, 14, 60],
"circle-opacity": ["step", ["zoom"], 0.8, 7, 0],
}}
/>
<Layer
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": [
"interpolate",
["linear"],
["zoom"],
7,
0.5,
12,
1,
],
}}
minzoom={7}
maxzoom={12}
/>
<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": [
"concat",
"marker-",
["get", "id"]
],
"icon-size": [
"interpolate",
["linear"],
["zoom"],
12,
0.4,
14,
0.8,
],
}}
minzoom={12}
/>
</Source>
<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>
{/* Search mobile */}
<Paper
shadow={"md"}
radius={0}
display={!openedMenu && 'none'}
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
height: "340px",
opacity: "95%",
padding: "55px 0 1rem 1rem"
}}
sx={{
"@media (min-width: 60em)": {
display: "none",
},
}}
>
<Autocomplete
mr={20}
mb={10}
placeholder="Поиск"
limit={2}
value={search}
onChange={setSearch}
data={[]}
/>
<ScrollArea h={"225px"} type="auto">
<Flex
// mih={50}
gap="xs"
justify="flex-start"
align="start"
direction="column"
wrap="wrap"
mr={20}
>
{articles !== null &&
articles.length > 0 &&
articles.map((article) => (
<ArticleCardMobile
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>
{/* Search pc */}
<Paper
shadow={"md"}
withBorder
style={{
position: "absolute",
top: "1rem",
left: "1rem",
width: "36rem",
height: "83vh",
opacity: "95%",
padding: "1rem 0 1rem 1rem",
}}
sx={{
"@media (max-width: 60em)": {
display: "none",
},
}}
>
<Title order={3} mb={10}>
Музеи
</Title>
<Autocomplete
mr={20}
mb={30}
placeholder="Поиск"
limit={2}
value={search}
onChange={setSearch}
data={[]}
/>
<ScrollArea h={"67vh"} type="auto">
<Flex
mih={50}
gap="md"
justify="flex-start"
align="start"
direction="column"
wrap="wrap"
mr={20}
>
{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>
<FooterLinks data={footerLinks.data} />
</>
);
}