parent
efef3fbf8e
commit
81b434b6a2
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -1,93 +1,8 @@
|
|||||||
import "./App.css";
|
import "./App.css";
|
||||||
import Map, { Layer, Popup, Source } from "react-map-gl";
|
import { MapComponent } from "./Map/MapComponent";
|
||||||
import maplibregl from "maplibre-gl";
|
|
||||||
|
|
||||||
import "maplibre-gl/dist/maplibre-gl.css";
|
|
||||||
import { useRef, useState } from "react";
|
|
||||||
import {
|
|
||||||
clusterCountLayer,
|
|
||||||
clusterLayer,
|
|
||||||
unclusteredPointLayer,
|
|
||||||
} from "./clusters";
|
|
||||||
import { countoursLayer } from "./vector-tile";
|
|
||||||
import { NavigateButton } from "./NavigateButton";
|
|
||||||
|
|
||||||
const MAP_TILER_KEY = "hE7PBueqYiS7hKSYUXP9";
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const mapRef = useRef(null);
|
return <MapComponent />;
|
||||||
const [showPopup, setShowPopup] = useState(true);
|
|
||||||
|
|
||||||
const onClick = (event) => {
|
|
||||||
try {
|
|
||||||
console.log(event.features);
|
|
||||||
const feature = event.features[0];
|
|
||||||
const clusterId = feature.properties.cluster_id;
|
|
||||||
|
|
||||||
const mapboxSource = mapRef.current.getSource("earthquakes");
|
|
||||||
|
|
||||||
mapboxSource.getClusterExpansionZoom(clusterId, (err, zoom) => {
|
|
||||||
if (err) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mapRef.current.easeTo({
|
|
||||||
center: feature.geometry.coordinates,
|
|
||||||
zoom,
|
|
||||||
duration: 500,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Map
|
|
||||||
mapLib={maplibregl}
|
|
||||||
mapStyle={`https://api.maptiler.com/maps/voyager/style.json?key=${MAP_TILER_KEY}`}
|
|
||||||
style={{ width: "100vw", height: "100vh" }}
|
|
||||||
initialViewState={{
|
|
||||||
latitude: 40.67,
|
|
||||||
longitude: -103.59,
|
|
||||||
zoom: 3,
|
|
||||||
}}
|
|
||||||
ref={mapRef}
|
|
||||||
interactiveLayerIds={[clusterLayer.id]}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
{showPopup && (
|
|
||||||
<Popup
|
|
||||||
longitude={-100}
|
|
||||||
latitude={40}
|
|
||||||
anchor="bottom"
|
|
||||||
onClose={() => setShowPopup(false)}
|
|
||||||
>
|
|
||||||
<div style={{ color: "red", fontWeight: "bold" }}>You are here</div>
|
|
||||||
</Popup>
|
|
||||||
)}
|
|
||||||
<Source
|
|
||||||
id="earthquakes"
|
|
||||||
type="geojson"
|
|
||||||
data="https://docs.mapbox.com/mapbox-gl-js/assets/earthquakes.geojson"
|
|
||||||
cluster={true}
|
|
||||||
clusterMaxZoom={14}
|
|
||||||
clusterRadius={50}
|
|
||||||
>
|
|
||||||
<Layer {...clusterLayer} />
|
|
||||||
<Layer {...clusterCountLayer} />
|
|
||||||
<Layer {...unclusteredPointLayer} />
|
|
||||||
</Source>
|
|
||||||
<Source
|
|
||||||
id="countours"
|
|
||||||
type="vector"
|
|
||||||
url="https://api.maptiler.com/tiles/contours/tiles.json?key=hE7PBueqYiS7hKSYUXP9"
|
|
||||||
>
|
|
||||||
<Layer {...countoursLayer} />
|
|
||||||
</Source>
|
|
||||||
<NavigateButton />
|
|
||||||
</Map>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
@ -0,0 +1,91 @@
|
|||||||
|
import maplibregl from "maplibre-gl";
|
||||||
|
import Map, { Layer, Popup, Source } from "react-map-gl";
|
||||||
|
import { useRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
clusterCountLayer,
|
||||||
|
clusterLayer,
|
||||||
|
unclusteredPointLayer,
|
||||||
|
} from "../clusters";
|
||||||
|
import { countoursLayer } from "../vector-tile";
|
||||||
|
import { NavigateButton } from "../NavigateButton";
|
||||||
|
import { Sidebar } from "../modules/Sidebar/Sidebar";
|
||||||
|
|
||||||
|
const MAP_TILER_KEY = "hE7PBueqYiS7hKSYUXP9";
|
||||||
|
|
||||||
|
export const MapComponent = () => {
|
||||||
|
const mapRef = useRef(null);
|
||||||
|
const [showPopup, setShowPopup] = useState(true);
|
||||||
|
|
||||||
|
const onClick = (event) => {
|
||||||
|
try {
|
||||||
|
const feature = event.features[0];
|
||||||
|
if (!feature) return;
|
||||||
|
|
||||||
|
const clusterId = feature.properties.cluster_id;
|
||||||
|
|
||||||
|
const mapboxSource = mapRef.current.getSource("earthquakes");
|
||||||
|
|
||||||
|
mapboxSource.getClusterExpansionZoom(clusterId, (err, zoom) => {
|
||||||
|
if (err) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mapRef.current.easeTo({
|
||||||
|
center: feature.geometry.coordinates,
|
||||||
|
zoom,
|
||||||
|
duration: 500,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Map
|
||||||
|
mapLib={maplibregl}
|
||||||
|
mapStyle={`https://api.maptiler.com/maps/voyager/style.json?key=${MAP_TILER_KEY}`}
|
||||||
|
style={{ width: "100vw", height: "100vh" }}
|
||||||
|
initialViewState={{
|
||||||
|
latitude: 55.7558,
|
||||||
|
longitude: 37.6173,
|
||||||
|
zoom: 10,
|
||||||
|
}}
|
||||||
|
ref={mapRef}
|
||||||
|
interactiveLayerIds={[clusterLayer.id]}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{showPopup && (
|
||||||
|
<Popup
|
||||||
|
longitude={-100}
|
||||||
|
latitude={40}
|
||||||
|
anchor="bottom"
|
||||||
|
onClose={() => setShowPopup(false)}
|
||||||
|
>
|
||||||
|
<div style={{ color: "red", fontWeight: "bold" }}>You are here</div>
|
||||||
|
</Popup>
|
||||||
|
)}
|
||||||
|
<Source
|
||||||
|
id="earthquakes"
|
||||||
|
type="geojson"
|
||||||
|
data="https://docs.mapbox.com/mapbox-gl-js/assets/earthquakes.geojson"
|
||||||
|
cluster={true}
|
||||||
|
clusterMaxZoom={14}
|
||||||
|
clusterRadius={50}
|
||||||
|
>
|
||||||
|
<Layer {...clusterLayer} />
|
||||||
|
<Layer {...clusterCountLayer} />
|
||||||
|
<Layer {...unclusteredPointLayer} />
|
||||||
|
</Source>
|
||||||
|
<Source
|
||||||
|
id="countours"
|
||||||
|
type="vector"
|
||||||
|
url="https://api.maptiler.com/tiles/contours/tiles.json?key=hE7PBueqYiS7hKSYUXP9"
|
||||||
|
>
|
||||||
|
<Layer {...countoursLayer} />
|
||||||
|
</Source>
|
||||||
|
<NavigateButton />
|
||||||
|
<Sidebar />
|
||||||
|
</Map>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { Typography } from "antd";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
export const Title = ({ text, className }) => {
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<Text type="secondary" className={twMerge("uppercase")}>
|
||||||
|
{text}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,70 +1,3 @@
|
|||||||
:root {
|
@tailwind base;
|
||||||
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
@tailwind components;
|
||||||
font-size: 16px;
|
@tailwind utilities;
|
||||||
line-height: 24px;
|
|
||||||
font-weight: 400;
|
|
||||||
|
|
||||||
color-scheme: light dark;
|
|
||||||
color: rgba(255, 255, 255, 0.87);
|
|
||||||
background-color: #242424;
|
|
||||||
|
|
||||||
font-synthesis: none;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
-webkit-text-size-adjust: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #646cff;
|
|
||||||
text-decoration: inherit;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #535bf2;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
min-width: 320px;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 3.2em;
|
|
||||||
line-height: 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
padding: 0.6em 1.2em;
|
|
||||||
font-size: 1em;
|
|
||||||
font-weight: 500;
|
|
||||||
font-family: inherit;
|
|
||||||
background-color: #1a1a1a;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: border-color 0.25s;
|
|
||||||
}
|
|
||||||
button:hover {
|
|
||||||
border-color: #646cff;
|
|
||||||
}
|
|
||||||
button:focus,
|
|
||||||
button:focus-visible {
|
|
||||||
outline: 4px auto -webkit-focus-ring-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
|
||||||
:root {
|
|
||||||
color: #213547;
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #747bff;
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
import React from 'react'
|
import React from "react";
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from "react-dom/client";
|
||||||
import App from './App'
|
import App from "./App";
|
||||||
import './index.css'
|
import "maplibre-gl/dist/maplibre-gl.css";
|
||||||
|
import "antd/dist/antd.css";
|
||||||
|
import "./index.css";
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<App />
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
)
|
);
|
||||||
|
|||||||
@ -0,0 +1,67 @@
|
|||||||
|
import { Button } from "antd";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Title } from "../../components/Title";
|
||||||
|
|
||||||
|
const types = [
|
||||||
|
{ id: "kiosk", name: "Городские киоски" },
|
||||||
|
{ id: "mfc", name: "МФЦ" },
|
||||||
|
{ id: "library", name: "Библиотеки" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const SelectItem = ({ name, isActive, onClick }) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
type="text"
|
||||||
|
className={twMerge(
|
||||||
|
"text-left",
|
||||||
|
isActive && "bg-blue hover:bg-blue active:bg-blue focus:bg-blue"
|
||||||
|
)}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ObjectTypesSelect = () => {
|
||||||
|
const [activeTypes, setActiveTypes] = useState([]);
|
||||||
|
|
||||||
|
const handleClick = (typeId) =>
|
||||||
|
setActiveTypes((activeTypes) =>
|
||||||
|
activeTypes.includes(typeId)
|
||||||
|
? activeTypes.filter((id) => id !== typeId)
|
||||||
|
: [...activeTypes, typeId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const clear = () => setActiveTypes([]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between items-center mb-1">
|
||||||
|
<Title text={"Тип объекта размещения"} />
|
||||||
|
{activeTypes.length !== 0 && (
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
className="text-grey text-xs p-0 hover:bg-transparent active:bg-transparent focus:bg-transparent h-fit"
|
||||||
|
onClick={clear}
|
||||||
|
>
|
||||||
|
сбросить
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
{types.map((type) => (
|
||||||
|
<SelectItem
|
||||||
|
key={type.id}
|
||||||
|
name={type.name}
|
||||||
|
isActive={activeTypes.includes(type.id)}
|
||||||
|
onClick={() => handleClick(type.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { Slider } from "antd";
|
||||||
|
import { Title } from "../../components/Title";
|
||||||
|
|
||||||
|
export const RatingSlider = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Title
|
||||||
|
text={"Востребованность постамата, усл. ед."}
|
||||||
|
className="flex justify-center"
|
||||||
|
/>
|
||||||
|
<Slider range defaultValue={[20, 50]} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
import { RegionSelect } from "./RegionSelect";
|
||||||
|
import { Button, Checkbox } from "antd";
|
||||||
|
import { BsChevronLeft } from "react-icons/bs";
|
||||||
|
import { ObjectTypesSelect } from "./ObjectTypesSelect";
|
||||||
|
import { RatingSlider } from "./RatingSlider";
|
||||||
|
|
||||||
|
export const Sidebar = () => {
|
||||||
|
return (
|
||||||
|
<div className="absolute top-10 right-10 bg-white-background w-[300px] rounded-xl p-3">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<BsChevronLeft className="mr-3" />}
|
||||||
|
className="flex items-center p-0 pr-1 text-grey mb-2"
|
||||||
|
>
|
||||||
|
Настройки
|
||||||
|
</Button>
|
||||||
|
<div className="space-y-5">
|
||||||
|
<RegionSelect />
|
||||||
|
<ObjectTypesSelect />
|
||||||
|
<Checkbox>Тепловая карта</Checkbox>
|
||||||
|
<RatingSlider />
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Button type="primary">Экспорт данных</Button>
|
||||||
|
<Button type="primary">Экспорт карты</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: ["./src/**/*.{js,jsx}"],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
'white-background': 'rgba(255, 255, 255, 0.8)',
|
||||||
|
'blue': '#8fc6f9',
|
||||||
|
'grey': 'rgba(0,0, 0, 0.5)'
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
corePlugins: {
|
||||||
|
preflight: false // <== disable this!
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from "vite";
|
||||||
import react from '@vitejs/plugin-react'
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()]
|
plugins: [react()],
|
||||||
})
|
});
|
||||||
|
|||||||
Loading…
Reference in new issue