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 { gridLayer } from "./layers-config";
import { useGridSize } from "../stores/useGridSize"; import { useGridSize } from "../stores/useGridSize";
import { useLayersVisibility } from "../stores/useLayersVisibility"; import { useLayersVisibility } from "../stores/useLayersVisibility";
import { useFactors } from "../stores/useFactors";
import { useMemo } from "react"; import { useMemo } from "react";
import { useRateExpression } from "./useRateExpression";
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];
};
export const Grid = ({ rate: rateRange }) => { export const Grid = ({ rate: rateRange }) => {
const { gridSize } = useGridSize(); const { gridSize } = useGridSize();
const { const {
isVisible: { grid }, isVisible: { grid },
} = useLayersVisibility(); } = useLayersVisibility();
const { factors } = useFactors();
const rate = useMemo(() => getRateExpression(factors), [factors]); const rate = useRateExpression();
const filter = useMemo(() => { const filter = useMemo(() => {
return ["all", [">=", rate, rateRange[0]], ["<=", rate, rateRange[1]]]; return ["all", [">=", rate, rateRange[0]], ["<=", rate, rateRange[1]]];
@ -50,9 +25,9 @@ export const Grid = ({ rate: rateRange }) => {
rate, rate,
0, 0,
"rgb(204,34,34)", "rgb(204,34,34)",
25, 10,
"rgb(255,221,52)", "rgb(255,221,52)",
40, 20,
"rgb(30,131,42)", "rgb(30,131,42)",
], ],
}; };

@ -1,13 +1,58 @@
import { Grid } from "./Grid"; import { Grid } from "./Grid";
import { useRating } from "../stores/useRating"; import { useRating } from "../stores/useRating";
import { Points } from "./Points"; 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 = () => { export const Layers = () => {
const { rate } = useRating(); const { rate } = useRating();
const { geometry } = useRegionGeometry();
const { isVisible } = useLayersVisibility();
return ( return (
<> <>
<Grid rate={rate} /> <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} /> <Points rate={rate} />
</> </>
); );

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

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

@ -46,3 +46,52 @@ export const gridLayer = {
"fill-outline-color": "transparent", "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, max = 100,
range = false, range = false,
step = 1, step = 1,
disabled = false,
}) => { }) => {
const fullRangeMarks = { const fullRangeMarks = {
[min]: <Mark value={min} />, [min]: <Mark value={min} />,
@ -76,6 +77,7 @@ export const SliderComponent = ({
min={min} min={min}
max={max} max={max}
step={step} step={step}
disabled={disabled}
/> />
</div> </div>
); );

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

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

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

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

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

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

@ -7,80 +7,92 @@ import { LayersVisibility } from "./LayersVisibility";
import { GridSizeSelect } from "./GridSizeSelect"; import { GridSizeSelect } from "./GridSizeSelect";
import { ModelSelect } from "./ModelSelect"; import { ModelSelect } from "./ModelSelect";
import { Settings } from "./Settings"; 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 = { const activeTablesMapper = {
// net_3: ["point3", "net_3"], net_3: ["point3", "net_3"],
// net_4: ["point4", "net_4"], net_4: ["point4", "net_4"],
// net_5: ["point5", "net_5"], 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 getRegionParam = (regionId) => {
// const link = document.createElement("a"); if (!regionId) return null;
// 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) => { const [type, id] = regionId.split("-");
// if (!regionId) return null;
// if (type === "ao") {
// const [type, id] = regionId.split("-"); return { msk_ao: Number(id) };
// }
// if (type === "ao") {
// return { msk_ao: Number(id) }; return { msk_rayon: Number(id) };
// } };
//
// return { msk_rayon: Number(id) };
// };
export const Sidebar = () => { export const Sidebar = () => {
// const { factors } = useFactors(); const { factors } = useFactors();
// const { activeTypes } = useActiveTypes(); const { activeTypes } = useActiveTypes();
// const { region } = useRegion(); const { region } = useRegion();
// const { gridSize } = useGridSize(); const { gridSize } = useGridSize();
// const { rate: rateRange } = useRating();
// const handleExport = async () => {
// const params = { const [isExporting, setIsExporting] = useState(false);
// koefs: factors,
// tables: activeTablesMapper[gridSize], const handleExport = async () => {
// }; setIsExporting(true);
// try {
// if (region) { const params = {
// params.filters = { koefs: factors,
// ...params.filters, tables: activeTablesMapper[gridSize],
// ...getRegionParam(region), filters: {
// }; rate_from: rateRange[0] / 100,
// } rate_to: rateRange[1] / 100,
// },
// if (activeTypes.length) { };
// params.filters = {
// ...params.filters, if (region) {
// category: activeTypes, params.filters = {
// }; ...params.filters,
// } ...getRegionParam(region),
// };
// const resp = await api.post("/api/raschet/", params); }
// const blob = resp.data;
// if (activeTypes.length) {
// const downloadLink = window.document.createElement("a"); params.filters = {
// downloadLink.href = window.URL.createObjectURL( ...params.filters,
// new Blob([blob], { category: activeTypes,
// type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", };
// }) }
// );
// downloadLink.download = "postamates.xlsx"; const resp = await api.post("/api/raschet/", params, {
// document.body.appendChild(downloadLink); responseType: "arraybuffer",
// downloadLink.click(); });
// document.body.removeChild(downloadLink); const blob = resp.data;
// };
download("postamates.xlsx", blob);
} catch (err) {
console.log("Произошла ошибка");
} finally {
setIsExporting(false);
}
};
return ( 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"> <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 /> <ObjectTypesSelect />
<RatingSlider /> <RatingSlider />
<div> <div>
<Button type="primary" block className={"mt-2"} disabled> <Button
type="primary"
block
className={"mt-2"}
onClick={handleExport}
loading={isExporting}
>
Экспорт данных Экспорт данных
</Button> </Button>
</div> </div>

@ -4,6 +4,7 @@ import { immer } from "zustand/middleware/immer";
const INITIAL_STATE = { const INITIAL_STATE = {
points: false, points: false,
grid: true, grid: true,
atd: true,
}; };
const store = (set) => ({ 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"; import { immer } from "zustand/middleware/immer";
const store = (set) => ({ const store = (set) => ({
rate: [0, 45], rate: [0, 100],
setRate: (value) => setRate: (value) =>
set((state) => { set((state) => {
state.rate = value; 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