Export data; change model; use circle radius to highlight rate

dev
Platon Yasev 3 years ago
parent 00f21ffcd9
commit e7d2a20d18

@ -2,41 +2,16 @@ import { Layer, Source } from "react-map-gl";
import { gridLayer } from "./layers-config";
import { useGridSize } from "../stores/useGridSize";
import { useLayersVisibility } from "../stores/useLayersVisibility";
import { useFactors } from "../stores/useFactors";
import { useMemo } from "react";
const getWeightedValueExpression = (factor, weight) => {
return ["*", ["to-number", ["get", factor]], weight];
};
export const getRateExpression = (factorWeights) => {
const weightSum = Object.entries(factorWeights).reduce(
(acc, [_factor, weight]) => {
acc += weight;
return acc;
},
0
);
const weightedValuesSum = Object.entries(factorWeights).reduce(
(acc, [factor, weight]) => {
acc.push(getWeightedValueExpression(factor, weight));
return acc;
},
["+"]
);
return ["/", weightedValuesSum, weightSum];
};
import { useRateExpression } from "./useRateExpression";
export const Grid = ({ rate: rateRange }) => {
const { gridSize } = useGridSize();
const {
isVisible: { grid },
} = useLayersVisibility();
const { factors } = useFactors();
const rate = useMemo(() => getRateExpression(factors), [factors]);
const rate = useRateExpression();
const filter = useMemo(() => {
return ["all", [">=", rate, rateRange[0]], ["<=", rate, rateRange[1]]];
@ -50,9 +25,9 @@ export const Grid = ({ rate: rateRange }) => {
rate,
0,
"rgb(204,34,34)",
25,
10,
"rgb(255,221,52)",
40,
20,
"rgb(30,131,42)",
],
};

@ -1,13 +1,58 @@
import { Grid } from "./Grid";
import { useRating } from "../stores/useRating";
import { Points } from "./Points";
import { Layer, Source } from "react-map-gl";
import { aoLayer, rayonLayer, selectedRegionLayer } from "./layers-config";
import { useRegionGeometry } from "../stores/useRegionGeometry";
import { useLayersVisibility } from "../stores/useLayersVisibility";
export const Layers = () => {
const { rate } = useRating();
const { geometry } = useRegionGeometry();
const { isVisible } = useLayersVisibility();
return (
<>
<Grid rate={rate} />
<Source
id="ao"
type="vector"
tiles={[
"https://postamates.spatiality.website/martin/public.msk_ao/{z}/{x}/{y}.pbf",
]}
>
<Layer
{...aoLayer}
layout={{
...aoLayer.layout,
visibility: isVisible.atd ? "visible" : "none",
}}
/>
</Source>
<Source
id="rayon"
type="vector"
tiles={[
"https://postamates.spatiality.website/martin/public.msk_rayoni/{z}/{x}/{y}.pbf",
]}
>
<Layer
{...rayonLayer}
layout={{
...rayonLayer.layout,
visibility: isVisible.atd ? "visible" : "none",
}}
/>
</Source>
<Source id="selected-region" type="geojson" data={geometry}>
<Layer
{...selectedRegionLayer}
layout={{
...selectedRegionLayer.layout,
visibility: geometry ? "visible" : "none",
}}
/>
</Source>
<Points rate={rate} />
</>
);

@ -3,17 +3,15 @@ import { pointLayer } from "./layers-config";
import { useLayersVisibility } from "../stores/useLayersVisibility";
import { useActiveTypes } from "../stores/useActiveTypes";
import { useGridSize } from "../stores/useGridSize";
import { useFactors } from "../stores/useFactors";
import { useMemo } from "react";
import { getRateExpression } from "./Grid";
import { useRateExpression } from "./useRateExpression";
export const Points = ({ rate: rateRange }) => {
const { gridSize } = useGridSize();
const { isVisible } = useLayersVisibility();
const { activeTypes } = useActiveTypes();
const { factors } = useFactors();
const rate = useMemo(() => getRateExpression(factors), [factors]);
const rate = useRateExpression();
const filter = useMemo(() => {
let result = [
@ -34,6 +32,11 @@ export const Points = ({ rate: rateRange }) => {
return result;
}, [rate, rateRange, activeTypes]);
const paintConfig = {
...pointLayer.paint,
"circle-radius": ["interpolate", ["linear"], rate, 0, 0, 10, 5, 50, 20],
};
return (
<>
<Source
@ -54,6 +57,7 @@ export const Points = ({ rate: rateRange }) => {
isVisible.points && gridSize === "net_3" ? "visible" : "none",
}}
filter={filter}
paint={paintConfig}
/>
</Source>
<Source
@ -74,6 +78,7 @@ export const Points = ({ rate: rateRange }) => {
isVisible.points && gridSize === "net_4" ? "visible" : "none",
}}
filter={filter}
paint={paintConfig}
/>
</Source>
<Source
@ -94,6 +99,7 @@ export const Points = ({ rate: rateRange }) => {
isVisible.points && gridSize === "net_5" ? "visible" : "none",
}}
filter={filter}
paint={paintConfig}
/>
</Source>
</>

@ -3,24 +3,37 @@ import { Col, Row } from "antd";
import { twMerge } from "tailwind-merge";
import { TYPE_MAPPER } from "../config";
import { useFactors } from "../stores/useFactors";
import { useMemo } from "react";
import { useModel } from "../stores/useModel";
const getRate = (featureProps, weights) => {
const weightedSum = Object.entries(weights).reduce(
(acc, [factor, weight]) => {
const value = Number(featureProps[factor]);
const weightedValue = value * weight;
acc += weightedValue;
const useRateValue = (feature) => {
const { factors: weights } = useFactors();
const { model } = useModel();
const result = useMemo(() => {
if (model === "ml") {
return feature.properties.model;
}
const weightedSum = Object.entries(weights).reduce(
(acc, [factor, weight]) => {
const value = Number(feature.properties[factor]);
const weightedValue = value * weight;
acc += weightedValue;
return acc;
},
0
);
const weightSum = Object.values(weights).reduce((acc, weight) => {
acc += weight;
return acc;
},
0
);
}, 0);
const weightSum = Object.values(weights).reduce((acc, weight) => {
acc += weight;
return acc;
}, 0);
return weightedSum / weightSum;
}, [weights, feature, model]);
return weightedSum / weightSum;
return result;
};
const pointConfig = [
@ -29,14 +42,6 @@ const pointConfig = [
name: "Тип",
formatter: (value) => TYPE_MAPPER[value],
},
// {
// field: "msk_ao",
// name: "АО",
// },
// {
// field: "msk_rayon",
// name: "Район",
// },
{
name: "Востребованность, у.е.",
formatter: (value) => Math.round(value),
@ -54,9 +59,9 @@ export const MapPopup = ({ feature, lat, lng, onClose }) => {
const isPoint = feature.geometry.type === "Point";
const config = isPoint ? pointConfig : gridConfig;
const layout = isPoint
? { width: 300, keyCol: 15, valueCol: 9 }
: { width: 250, keyCol: 20, valueCol: 4 };
const { factors } = useFactors();
? { keyCol: 15, valueCol: 9 }
: { keyCol: 20, valueCol: 4 };
const rate = useRateValue(feature);
return (
<Popup
@ -66,11 +71,9 @@ export const MapPopup = ({ feature, lat, lng, onClose }) => {
onClose={onClose}
closeOnClick={false}
>
<div className={`!min-w-[${layout.width}px]`}>
<div>
{config.map((item) => {
const value = item.field
? feature.properties[item.field]
: getRate(feature.properties, factors);
const value = item.field ? feature.properties[item.field] : rate;
return (
<Row className={twMerge("p-1")} key={item.field ?? "rate"}>

@ -46,3 +46,52 @@ export const gridLayer = {
"fill-outline-color": "transparent",
},
};
const regionColor = "#676767";
export const aoLayer = {
id: "ao",
type: "line",
source: "ao",
"source-layer": "public.msk_ao",
layout: {
"line-join": "round",
"line-cap": "round",
},
paint: {
"line-color": regionColor,
"line-width": 1.5,
"line-opacity": 0.8,
},
};
export const rayonLayer = {
id: "rayon",
type: "line",
source: "rayon",
"source-layer": "public.msk_rayoni",
layout: {
"line-join": "round",
"line-cap": "round",
},
paint: {
"line-color": regionColor,
"line-width": 0.5,
"line-opacity": 0.8,
},
};
export const selectedRegionLayer = {
id: "selected-region",
type: "line",
source: "selected-region",
layout: {
"line-join": "round",
"line-cap": "round",
},
paint: {
"line-color": "#47006e",
"line-width": 2,
"line-opacity": 1,
},
};

@ -0,0 +1,38 @@
import { useMemo } from "react";
import { useFactors } from "../stores/useFactors";
import { useModel } from "../stores/useModel";
const getWeightedValueExpression = (factor, weight) => {
return ["*", ["to-number", ["get", factor]], weight];
};
export const useRateExpression = () => {
const { factors } = useFactors();
const { model } = useModel();
const result = useMemo(() => {
if (model === "ml") {
return ["get", "model"];
}
const weightSum = Object.entries(factors).reduce(
(acc, [_factor, weight]) => {
acc += weight;
return acc;
},
0
);
const weightedValuesSum = Object.entries(factors).reduce(
(acc, [factor, weight]) => {
acc.push(getWeightedValueExpression(factor, weight));
return acc;
},
["+"]
);
return ["/", weightedValuesSum, weightSum];
}, [factors, model]);
return result;
};

@ -31,6 +31,7 @@ export const SliderComponent = ({
max = 100,
range = false,
step = 1,
disabled = false,
}) => {
const fullRangeMarks = {
[min]: <Mark value={min} />,
@ -76,6 +77,7 @@ export const SliderComponent = ({
min={min}
max={max}
step={step}
disabled={disabled}
/>
</div>
);

@ -4,7 +4,7 @@
.mapboxgl-popup,
.maplibregl-popup {
@apply !max-w-[400px];
@apply max-w-[400px] min-w-[250px];
}
.mapboxgl-popup-content,

@ -8,7 +8,7 @@ export const LayersVisibility = () => {
return (
<div>
<Title text={"Слои"} className={"mb-1"} />
<div className={"space-y-1"}>
<div className={"space-y-1 flex flex-col"}>
<Checkbox
onChange={() => toggleVisibility("points")}
checked={isVisible.points}
@ -23,6 +23,13 @@ export const LayersVisibility = () => {
>
Тепловая карта
</Checkbox>
<Checkbox
className={"!ml-0"}
onChange={() => toggleVisibility("atd")}
checked={isVisible.atd}
>
Единицы АТД
</Checkbox>
</div>
</div>
);

@ -1,6 +1,6 @@
import { Select } from "antd";
import { Title } from "../../components/Title";
import { useState } from "react";
import { useModel } from "../../stores/useModel";
const options = [
{ value: "statistic", label: "Обычная" },
@ -8,18 +8,17 @@ const options = [
];
export const ModelSelect = () => {
const [value, setValue] = useState("statistic");
const handleChange = (newValue) => setValue(newValue);
const { model, setModel } = useModel();
const handleChange = (newValue) => setModel(newValue);
return (
<div className={"flex flex-col items-center"}>
<Title text={"Модель расчета"} />
<Select
className={"w-full"}
value={value}
value={model}
onChange={handleChange}
options={options}
disabled={true}
/>
</div>
);

@ -11,7 +11,7 @@ export const RatingSlider = () => {
title={"Востребованность постамата, у.e."}
value={rate}
onAfterChange={handleAfterChange}
max={45}
max={100}
range
/>
);

@ -6,6 +6,7 @@ import { api } from "../../config";
import { useMap } from "react-map-gl";
import getBbox from "@turf/bbox";
import { polygon as getPolygon } from "@turf/helpers";
import { useRegionGeometry } from "../../stores/useRegionGeometry";
const { TreeNode } = TreeSelect;
@ -26,6 +27,7 @@ const normalizeRegions = (rawRegions) => {
export const RegionSelect = () => {
const { current: map } = useMap();
const { region, setRegion } = useRegion();
const { setRegionGeometry } = useRegionGeometry();
const [data, setData] = useState([]);
const normalizedData = useMemo(() => normalizeRegions(data), [data]);
const [loading, setLoading] = useState(false);
@ -47,17 +49,30 @@ export const RegionSelect = () => {
}, []);
const onChange = (value) => {
if (!value) return;
const selectedRegion = normalizedData[value];
const polygon = getPolygon(selectedRegion.geometry[0]);
const bbox = getBbox(polygon);
setRegionGeometry(polygon);
setRegion(value);
map.fitBounds([
[bbox[0], bbox[1]], // southwestern corner of the bounds
[bbox[2], bbox[3]], // northeastern corner of the bounds
]);
map.fitBounds(
[
[bbox[0], bbox[1]], // southwestern corner of the bounds
[bbox[2], bbox[3]], // northeastern corner of the bounds
],
{
padding: 20,
}
);
};
const handleClear = () => {
setRegion(null);
setRegionGeometry(null);
};
return (
@ -75,6 +90,7 @@ export const RegionSelect = () => {
loading={loading}
// treeNodeFilterProp="name"
// filterTreeNode={true}
onClear={handleClear}
>
{data?.map((parent) => {
return (

@ -1,9 +1,11 @@
import { SliderComponent as Slider } from "../../components/SliderComponent";
import { useFactors } from "../../stores/useFactors";
import { factorsNameMapper } from "../../config";
import { useModel } from "../../stores/useModel";
export const Settings = () => {
const { factors, setWeight } = useFactors();
const { model } = useModel();
const handleAfterChange = (factor, value) => {
setWeight(factor, value);
@ -20,6 +22,7 @@ export const Settings = () => {
max={1}
step={0.01}
onAfterChange={(value) => handleAfterChange(field, value)}
disabled={model === "ml"}
/>
);
})}

@ -7,80 +7,92 @@ import { LayersVisibility } from "./LayersVisibility";
import { GridSizeSelect } from "./GridSizeSelect";
import { ModelSelect } from "./ModelSelect";
import { Settings } from "./Settings";
import { useFactors } from "../../stores/useFactors";
import { useActiveTypes } from "../../stores/useActiveTypes";
import { useRegion } from "../../stores/useRegion";
import { useGridSize } from "../../stores/useGridSize";
import { api } from "../../config";
import { useRating } from "../../stores/useRating";
import { useState } from "react";
// const activeTablesMapper = {
// net_3: ["point3", "net_3"],
// net_4: ["point4", "net_4"],
// net_5: ["point5", "net_5"],
// };
const activeTablesMapper = {
net_3: ["point3", "net_3"],
net_4: ["point4", "net_4"],
net_5: ["point5", "net_5"],
};
function download(filename, data) {
const downloadLink = window.document.createElement("a");
downloadLink.href = window.URL.createObjectURL(
new Blob([data], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
})
);
downloadLink.download = filename;
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
}
// function download(filename, data) {
// const link = document.createElement("a");
// link.href =
// "data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64," +
// encodeURIComponent(data);
// link.setAttribute("download", filename);
//
// link.style.display = "none";
// document.body.appendChild(link);
//
// link.click();
//
// document.body.removeChild(link);
// }
const getRegionParam = (regionId) => {
if (!regionId) return null;
// const getRegionParam = (regionId) => {
// if (!regionId) return null;
//
// const [type, id] = regionId.split("-");
//
// if (type === "ao") {
// return { msk_ao: Number(id) };
// }
//
// return { msk_rayon: Number(id) };
// };
const [type, id] = regionId.split("-");
if (type === "ao") {
return { msk_ao: Number(id) };
}
return { msk_rayon: Number(id) };
};
export const Sidebar = () => {
// const { factors } = useFactors();
// const { activeTypes } = useActiveTypes();
// const { region } = useRegion();
// const { gridSize } = useGridSize();
//
// const handleExport = async () => {
// const params = {
// koefs: factors,
// tables: activeTablesMapper[gridSize],
// };
//
// if (region) {
// params.filters = {
// ...params.filters,
// ...getRegionParam(region),
// };
// }
//
// if (activeTypes.length) {
// params.filters = {
// ...params.filters,
// category: activeTypes,
// };
// }
//
// const resp = await api.post("/api/raschet/", params);
// const blob = resp.data;
//
// const downloadLink = window.document.createElement("a");
// downloadLink.href = window.URL.createObjectURL(
// new Blob([blob], {
// type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
// })
// );
// downloadLink.download = "postamates.xlsx";
// document.body.appendChild(downloadLink);
// downloadLink.click();
// document.body.removeChild(downloadLink);
// };
const { factors } = useFactors();
const { activeTypes } = useActiveTypes();
const { region } = useRegion();
const { gridSize } = useGridSize();
const { rate: rateRange } = useRating();
const [isExporting, setIsExporting] = useState(false);
const handleExport = async () => {
setIsExporting(true);
try {
const params = {
koefs: factors,
tables: activeTablesMapper[gridSize],
filters: {
rate_from: rateRange[0] / 100,
rate_to: rateRange[1] / 100,
},
};
if (region) {
params.filters = {
...params.filters,
...getRegionParam(region),
};
}
if (activeTypes.length) {
params.filters = {
...params.filters,
category: activeTypes,
};
}
const resp = await api.post("/api/raschet/", params, {
responseType: "arraybuffer",
});
const blob = resp.data;
download("postamates.xlsx", blob);
} catch (err) {
console.log("Произошла ошибка");
} finally {
setIsExporting(false);
}
};
return (
<div className="absolute top-[20px] right-[20px] bg-white-background w-[300px] rounded-xl p-3 max-h-[calc(100vh-40px)] overflow-y-auto z-10">
@ -107,7 +119,13 @@ export const Sidebar = () => {
<ObjectTypesSelect />
<RatingSlider />
<div>
<Button type="primary" block className={"mt-2"} disabled>
<Button
type="primary"
block
className={"mt-2"}
onClick={handleExport}
loading={isExporting}
>
Экспорт данных
</Button>
</div>

@ -4,6 +4,7 @@ import { immer } from "zustand/middleware/immer";
const INITIAL_STATE = {
points: false,
grid: true,
atd: true,
};
const store = (set) => ({

@ -0,0 +1,12 @@
import create from "zustand";
import { immer } from "zustand/middleware/immer";
const store = (set) => ({
model: "statistic",
setModel: (value) =>
set((state) => {
state.model = value;
}),
});
export const useModel = create(immer(store));

@ -2,7 +2,7 @@ import create from "zustand";
import { immer } from "zustand/middleware/immer";
const store = (set) => ({
rate: [0, 45],
rate: [0, 100],
setRate: (value) =>
set((state) => {
state.rate = value;

@ -0,0 +1,12 @@
import create from "zustand";
import { immer } from "zustand/middleware/immer";
const store = (set) => ({
geometry: null,
setRegionGeometry: (value) =>
set((state) => {
state.geometry = value;
}),
});
export const useRegionGeometry = create(immer(store));
Loading…
Cancel
Save