From 8f3bd0eab826eeaa6cf6f8dafa508a445a954ecf Mon Sep 17 00:00:00 2001 From: Platon Yasev Date: Sun, 6 Nov 2022 16:28:38 +0300 Subject: [PATCH] Calculate rating --- package.json | 3 + src/Map/Grid.jsx | 75 +++++++++++++---- src/Map/MapComponent.jsx | 10 ++- src/Map/Points.jsx | 109 +++++++++++++++++++------ src/Map/Popup.jsx | 55 ++++++++++--- src/Map/layers-config.js | 16 +--- src/components/SliderComponent.jsx | 2 + src/config.js | 27 ++++++ src/index.css | 7 +- src/modules/Sidebar/GridSizeSelect.jsx | 6 +- src/modules/Sidebar/RatingSlider.jsx | 2 +- src/modules/Sidebar/RegionSelect.jsx | 84 +++++++++++++------ src/modules/Sidebar/Settings.jsx | 24 +++++- src/modules/Sidebar/Sidebar.jsx | 2 +- src/stores/useFactors.js | 20 +++++ src/stores/useGridSize.js | 2 +- src/stores/useRating.js | 2 +- yarn.lock | 39 +++++++++ 18 files changed, 381 insertions(+), 104 deletions(-) create mode 100644 src/stores/useFactors.js diff --git a/package.json b/package.json index 0c81b0e..397196e 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,11 @@ "preview": "vite preview" }, "dependencies": { + "@turf/bbox": "^6.5.0", + "@turf/helpers": "^6.5.0", "@watergis/maplibre-gl-export": "^1.3.7", "antd": "^4.23.6", + "axios": "^1.1.3", "immer": "^9.0.16", "mapbox-gl": "npm:empty-npm-package@1.0.0", "maplibre-gl": "^2.4.0", diff --git a/src/Map/Grid.jsx b/src/Map/Grid.jsx index 1c49bf9..a9d3892 100644 --- a/src/Map/Grid.jsx +++ b/src/Map/Grid.jsx @@ -2,18 +2,60 @@ 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"; -export const Grid = ({ rate }) => { +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 }) => { const { gridSize } = useGridSize(); const { isVisible: { grid }, } = useLayersVisibility(); + const { factors } = useFactors(); + + const rate = useMemo(() => getRateExpression(factors), [factors]); + + const filter = useMemo(() => { + return ["all", [">=", rate, rateRange[0]], ["<=", rate, rateRange[1]]]; + }, [rate, rateRange]); - const filter = [ - "all", - [">=", ["get", "rate"], rate[0]], - ["<=", ["get", "rate"], rate[1]], - ]; + const paintConfig = { + ...gridLayer.paint, + "fill-color": [ + "interpolate", + ["linear"], + rate, + 0, + "rgb(204,34,34)", + 25, + "rgb(255,221,52)", + 40, + "rgb(30,131,42)", + ], + }; return ( <> @@ -21,18 +63,19 @@ export const Grid = ({ rate }) => { id="grid3" type="vector" tiles={[ - `https://postamates.spatiality.website/martin/public.net3/{z}/{x}/{y}.pbf`, + `https://postamates.spatiality.website/martin/public.net_3/{z}/{x}/{y}.pbf`, ]} > @@ -40,38 +83,40 @@ export const Grid = ({ rate }) => { id="grid4" type="vector" tiles={[ - `https://postamates.spatiality.website/martin/public.net4/{z}/{x}/{y}.pbf`, + `https://postamates.spatiality.website/martin/public.net_4/{z}/{x}/{y}.pbf`, ]} > diff --git a/src/Map/MapComponent.jsx b/src/Map/MapComponent.jsx index ace3100..1426a2f 100644 --- a/src/Map/MapComponent.jsx +++ b/src/Map/MapComponent.jsx @@ -2,7 +2,6 @@ import maplibregl from "maplibre-gl"; import Map, { useControl } from "react-map-gl"; import { useRef, useState } from "react"; import { Sidebar } from "../modules/Sidebar/Sidebar"; -import { pointLayer } from "./layers-config"; import { Layers } from "./Layers"; import { MapPopup } from "./Popup"; import { MaplibreExportControl } from "@watergis/maplibre-gl-export"; @@ -113,7 +112,14 @@ export const MapComponent = () => { }} dragRotate={false} ref={mapRef} - interactiveLayerIds={[pointLayer.id, "grid3", "grid4", "grid5"]} + interactiveLayerIds={[ + "point3", + "point4", + "point5", + "grid3", + "grid4", + "grid5", + ]} onClick={handleClick} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} diff --git a/src/Map/Points.jsx b/src/Map/Points.jsx index b41f7e8..9e784bd 100644 --- a/src/Map/Points.jsx +++ b/src/Map/Points.jsx @@ -2,35 +2,96 @@ import { Layer, Source } from "react-map-gl"; 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"; -export const Points = ({ rate }) => { +export const Points = ({ rate: rateRange }) => { + const { gridSize } = useGridSize(); const { isVisible } = useLayersVisibility(); const { activeTypes } = useActiveTypes(); + const { factors } = useFactors(); - const getFilter = () => { - if (activeTypes.length) { - return ["in", "category", ...activeTypes]; - } else { - return ["all"]; - } - }; + const rate = useMemo(() => getRateExpression(factors), [factors]); + + const filter = useMemo(() => { + let result = [ + "all", + [">=", rate, rateRange[0]], + ["<=", rate, rateRange[1]], + ]; + + // if (activeTypes.length) { + // result = ["all", result, ["in", "category", ...activeTypes]]; + // } + + console.log(result); + return result; + }, [rate, rateRange, activeTypes]); return ( - - - + <> + + + + + + + + + + ); }; diff --git a/src/Map/Popup.jsx b/src/Map/Popup.jsx index 3e14b43..18935fe 100644 --- a/src/Map/Popup.jsx +++ b/src/Map/Popup.jsx @@ -2,6 +2,26 @@ import { Popup } from "react-map-gl"; import { Col, Row } from "antd"; import { twMerge } from "tailwind-merge"; import { TYPE_MAPPER } from "../config"; +import { useFactors } from "../stores/useFactors"; + +const getRate = (featureProps, weights) => { + const weightedSum = Object.entries(weights).reduce( + (acc, [factor, weight]) => { + const value = Number(featureProps[factor]); + const weightedValue = value * weight; + acc += weightedValue; + return acc; + }, + 0 + ); + + const weightSum = Object.values(weights).reduce((acc, weight) => { + acc += weight; + return acc; + }, 0); + + return weightedSum / weightSum; +}; const pointConfig = [ { @@ -9,27 +29,34 @@ const pointConfig = [ name: "Тип", formatter: (value) => TYPE_MAPPER[value], }, + // { + // field: "msk_ao", + // name: "АО", + // }, + // { + // field: "msk_rayon", + // name: "Район", + // }, { - field: "msk_ao", - name: "АО", - }, - { - field: "msk_rayon", - name: "Район", + name: "Востребованность, у.е.", + formatter: (value) => Math.round(value), }, ]; const gridConfig = [ { - field: "rate", name: "Востребованность, у.е.", + formatter: (value) => Math.round(value), }, ]; export const MapPopup = ({ feature, lat, lng, onClose }) => { const isPoint = feature.geometry.type === "Point"; const config = isPoint ? pointConfig : gridConfig; - const layout = isPoint ? [10, 14] : [20, 4]; + const layout = isPoint + ? { width: 300, keyCol: 15, valueCol: 9 } + : { width: 250, keyCol: 20, valueCol: 4 }; + const { factors } = useFactors(); return ( { onClose={onClose} closeOnClick={false} > -
+
{config.map((item) => { - const value = feature.properties[item.field]; + const value = item.field + ? feature.properties[item.field] + : getRate(feature.properties, factors); return ( - - + + {item.name} - + {item.formatter ? item.formatter(value) : value} diff --git a/src/Map/layers-config.js b/src/Map/layers-config.js index 13c570e..eb20df8 100644 --- a/src/Map/layers-config.js +++ b/src/Map/layers-config.js @@ -13,10 +13,7 @@ const pointColors = { }; export const pointLayer = { - id: "points", type: "circle", - source: "points", - "source-layer": "public.point3", layout: {}, paint: { "circle-color": [ @@ -45,18 +42,7 @@ export const gridLayer = { type: "fill", layout: {}, paint: { - "fill-color": [ - "interpolate", - ["linear"], - ["get", "rate"], - 0, - "rgb(204,34,34)", - 5, - "rgb(255,221,52)", - 10, - "rgb(30,131,42)", - ], - "fill-opacity": 0.3, + "fill-opacity": 0.5, "fill-outline-color": "transparent", }, }; diff --git a/src/components/SliderComponent.jsx b/src/components/SliderComponent.jsx index bb95397..3d9401f 100644 --- a/src/components/SliderComponent.jsx +++ b/src/components/SliderComponent.jsx @@ -30,6 +30,7 @@ export const SliderComponent = ({ min = 0, max = 100, range = false, + step = 1, }) => { const fullRangeMarks = { [min]: , @@ -74,6 +75,7 @@ export const SliderComponent = ({ onAfterChange={handleAfterChange} min={min} max={max} + step={step} />
); diff --git a/src/config.js b/src/config.js index 69a2ba5..15a66c9 100644 --- a/src/config.js +++ b/src/config.js @@ -1,3 +1,5 @@ +import axios from "axios"; + export const TYPE_MAPPER = { kiosk: "Городской киоск", mfc: "МФЦ", @@ -5,3 +7,28 @@ export const TYPE_MAPPER = { dk: "Дом культуры и отдыха", sport: "Спортивный объект", }; + +export const factorsNameMapper = { + people: "Численность населения в 2021 г.", + people2025: "Численность населения в 2025 г. (прогноз)", + stops_ot: "Остановки общественного транспорта", + routes_ot: "Маршруты общетвенного транспорта", + in_metro: "Входы в ближайшее метро в месяц", + out_metro: "Выходы из ближайшего метро в месяц", + tc: "Тогровые центры", + empls: "Рабочие места", + walkers: "Трафик населения", + schools: "Школы и детские сады", + parking: "Парковочные места", + pvz: "Пункты выдачи заказов", + gov_place: "Рекомендованные пункты размещения постаматов", + bike_park: "Городская аренда велосипедов, шт.", + products: "Продовольственные магазины", + nonprod: "Непродовольственные магазины", + service: "Пункты оказания бытовых услуг", + vuz: "ВУЗы и техникумы", +}; + +export const api = axios.create({ + baseURL: "https://postamates.spatiality.website", +}); diff --git a/src/index.css b/src/index.css index 46503db..b2a68cd 100644 --- a/src/index.css +++ b/src/index.css @@ -2,6 +2,11 @@ @tailwind components; @tailwind utilities; +.mapboxgl-popup, +.maplibregl-popup { + @apply !max-w-[400px]; +} + .mapboxgl-popup-content, .maplibregl-popup-content { @apply bg-grey-light shadow-lg rounded-md; @@ -13,7 +18,7 @@ } .ant-popover-inner { - @apply bg-white-background rounded-xl; + @apply bg-white-background rounded-xl max-h-[calc(100vh-100px)] overflow-y-auto; } .mapboxgl-ctrl-group, diff --git a/src/modules/Sidebar/GridSizeSelect.jsx b/src/modules/Sidebar/GridSizeSelect.jsx index 803d10e..d2af4cf 100644 --- a/src/modules/Sidebar/GridSizeSelect.jsx +++ b/src/modules/Sidebar/GridSizeSelect.jsx @@ -3,9 +3,9 @@ import { Title } from "../../components/Title"; import { useGridSize } from "../../stores/useGridSize"; const options = [ - { label: "3 мин", value: "net3" }, - { label: "4 мин", value: "net4" }, - { label: "5 мин", value: "net5" }, + { label: "3 мин", value: "net_3" }, + { label: "4 мин", value: "net_4" }, + { label: "5 мин", value: "net_5" }, ]; export const GridSizeSelect = () => { diff --git a/src/modules/Sidebar/RatingSlider.jsx b/src/modules/Sidebar/RatingSlider.jsx index d2dc684..005cd5f 100644 --- a/src/modules/Sidebar/RatingSlider.jsx +++ b/src/modules/Sidebar/RatingSlider.jsx @@ -11,7 +11,7 @@ export const RatingSlider = () => { title={"Востребованность постамата, у.e."} value={rate} onAfterChange={handleAfterChange} - max={10} + max={45} range /> ); diff --git a/src/modules/Sidebar/RegionSelect.jsx b/src/modules/Sidebar/RegionSelect.jsx index 839fc52..aa3d320 100644 --- a/src/modules/Sidebar/RegionSelect.jsx +++ b/src/modules/Sidebar/RegionSelect.jsx @@ -1,34 +1,63 @@ import { TreeSelect } from "antd"; import { Title } from "../../components/Title"; import { useRegion } from "../../stores/useRegion"; +import { useEffect, useMemo, useState } from "react"; +import { api } from "../../config"; +import { useMap } from "react-map-gl"; +import getBbox from "@turf/bbox"; +import { polygon as getPolygon } from "@turf/helpers"; const { TreeNode } = TreeSelect; -const mockRegions = [ - { - id: "tsao", - name: "Центральный (ЦАО)", - children: [ - { id: "arbat", name: "Арбат" }, - { id: "hamovniki", name: "Хамовники" }, - ], - }, - { - id: "yuao", - name: "Южный (ЮАО)", - children: [ - { id: "danilovsk", name: "Даниловский" }, - { id: "nagorn", name: "Нагорный" }, - ], - }, -]; +const normalizeRegions = (rawRegions) => { + if (!rawRegions) return {}; + + return rawRegions.reduce((acc, raw) => { + acc[`ao-${raw.id}`] = raw; + if (raw.children) { + raw.children.forEach((child) => { + acc[`rayon-${child.id}`] = child; + }); + } + return acc; + }, {}); +}; export const RegionSelect = () => { + const { current: map } = useMap(); const { region, setRegion } = useRegion(); + const [data, setData] = useState([]); + const normalizedData = useMemo(() => normalizeRegions(data), [data]); + const [loading, setLoading] = useState(false); + + useEffect(() => { + const getRegions = async () => { + setLoading(true); + try { + const response = await api.get("/api/ao_and_rayons"); + setData(response.data); + } catch (err) { + console.error(err); + } finally { + setLoading(false); + } + }; + + getRegions(); + }, []); const onChange = (value) => { - console.log(value); + const selectedRegion = normalizedData[value]; + + const polygon = getPolygon(selectedRegion.geometry[0]); + const bbox = getBbox(polygon); + setRegion(value); + + map.fitBounds([ + [bbox[0], bbox[1]], // southwestern corner of the bounds + [bbox[2], bbox[3]], // northeastern corner of the bounds + ]); }; return ( @@ -41,16 +70,25 @@ export const RegionSelect = () => { dropdownStyle={{ maxHeight: 400, overflow: "auto" }} placeholder="Выберите АО или район" allowClear - treeDefaultExpandAll + treeDefaultExpandAll={false} onChange={onChange} + loading={loading} // treeNodeFilterProp="name" // filterTreeNode={true} > - {mockRegions.map((parent) => { + {data?.map((parent) => { return ( - + {parent.children?.map((child) => ( - + ))} ); diff --git a/src/modules/Sidebar/Settings.jsx b/src/modules/Sidebar/Settings.jsx index e49eff6..50b4f71 100644 --- a/src/modules/Sidebar/Settings.jsx +++ b/src/modules/Sidebar/Settings.jsx @@ -1,13 +1,29 @@ import { SliderComponent as Slider } from "../../components/SliderComponent"; import { Button } from "antd"; +import { useFactors } from "../../stores/useFactors"; +import { factorsNameMapper } from "../../config"; export const Settings = () => { + const { factors, setWeight } = useFactors(); + + const handleAfterChange = (factor, value) => { + setWeight(factor, value); + }; + return (
- - - - + {Object.entries(factors).map(([field, value]) => { + return ( + handleAfterChange(field, value)} + /> + ); + })}