Add multiple selection

dev
Platon Yasev 3 years ago
parent 9390b15d81
commit 12c38c3295

@ -35,10 +35,7 @@ export const MapComponent = () => {
coordinates[0] += pointLng > coordinates[0] ? 360 : -360; coordinates[0] += pointLng > coordinates[0] ? 360 : -360;
} }
const popupFeature = { ...feature }; setPopup({ features: event.features, coordinates });
popupFeature.coordinates = coordinates;
setPopup(popupFeature);
} }
}; };
@ -86,7 +83,7 @@ export const MapComponent = () => {
}} }}
dragRotate={false} dragRotate={false}
ref={mapRef} ref={mapRef}
interactiveLayerIds={["points"]} interactiveLayerIds={["match-points", "unmatch-points"]}
onClick={handleClick} onClick={handleClick}
onMouseEnter={handleMouseEnter} onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
@ -96,7 +93,7 @@ export const MapComponent = () => {
<MapPopup <MapPopup
lat={popup.coordinates[1]} lat={popup.coordinates[1]}
lng={popup.coordinates[0]} lng={popup.coordinates[0]}
feature={popup.feature} features={popup.features}
onClose={() => setPopup(null)} onClose={() => setPopup(null)}
/> />
)} )}

@ -1,10 +1,38 @@
import { Layer, Source } from "react-map-gl"; import { Layer, Source } from "react-map-gl";
import { pointLayer } from "./layers-config"; import { pointLayer, unmatchPointLayer } from "./layers-config";
import { useLayersVisibility } from "../stores/useLayersVisibility"; import { useLayersVisibility } from "../stores/useLayersVisibility";
import { BASE_URL } from "../api"; import { BASE_URL } from "../api";
import { useFilters } from "../stores/useFilters";
import { usePointSelection } from "../stores/usePointSelection";
export const Points = () => { export const Points = () => {
const { isVisible } = useLayersVisibility(); const { isVisible } = useLayersVisibility();
const { filters } = useFilters();
const { prediction, status } = filters;
const { selection } = usePointSelection();
const includedArr = [...selection.included];
const excludedArr = [...selection.excluded];
const includedExpression = ["in", ["get", "id"], ["literal", includedArr]];
const excludedExpression = ["in", ["get", "id"], ["literal", excludedArr]];
const predictionExpression = [
[">=", ["get", "prediction_current"], prediction[0]],
["<=", ["get", "prediction_current"], prediction[1]],
];
const statusExpression = ["in", ["get", "status"], ["literal", status]];
const matchFilterExpression = [
"all",
statusExpression,
["!", excludedExpression],
["any", ["all", ...predictionExpression], includedExpression],
];
const unmatchFilterExpression = [
"all",
statusExpression,
["any", excludedExpression, ["!", ["all", ...predictionExpression]]],
];
return ( return (
<> <>
@ -17,13 +45,26 @@ export const Points = () => {
> >
<Layer <Layer
{...pointLayer} {...pointLayer}
id={"points"} id={"unmatch-points"}
source={"points"}
source-layer={"public.service_placementpoint"}
layout={{
...pointLayer.layout,
visibility: isVisible.points ? "visible" : "none",
}}
filter={unmatchFilterExpression}
paint={unmatchPointLayer.paint}
/>
<Layer
{...pointLayer}
id={"match-points"}
source={"points"} source={"points"}
source-layer={"public.service_placementpoint"} source-layer={"public.service_placementpoint"}
layout={{ layout={{
...pointLayer.layout, ...pointLayer.layout,
visibility: isVisible.points ? "visible" : "none", visibility: isVisible.points ? "visible" : "none",
}} }}
filter={matchFilterExpression}
paint={pointLayer.paint} paint={pointLayer.paint}
/> />
</Source> </Source>

@ -1,34 +1,25 @@
import { Popup } from "react-map-gl"; import { Popup } from "react-map-gl";
import { Button, Col, Row } from "antd"; import { Button, Col, Row } from "antd";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { TYPE_MAPPER } from "../config";
import { usePointSelection } from "../stores/usePointSelection"; import { usePointSelection } from "../stores/usePointSelection";
import { useState } from "react";
const pointConfig = [ const popupConfig = [
{ {
field: "name", field: "name",
name: "Название",
}, },
{ {
field: "category", field: "category",
name: "Тип", name: "Тип",
formatter: (value) => TYPE_MAPPER[value],
}, },
{ {
name: "Востребованность, у.е.", field: "prediction_current",
formatter: (value) => Math.round(value), name: "Прогнозный трафик",
}, },
]; ];
export const MapPopup = ({ feature, lat, lng, onClose }) => { const PopupWrapper = ({ lat, lng, onClose, children }) => {
const { include, selection, exclude } = usePointSelection();
const isSelected = selection.included.has(feature.properties.id);
const handleSelect = () =>
isSelected
? exclude(feature.properties.id)
: include(feature.properties.id);
return ( return (
<Popup <Popup
longitude={lng} longitude={lng}
@ -37,14 +28,38 @@ export const MapPopup = ({ feature, lat, lng, onClose }) => {
onClose={onClose} onClose={onClose}
closeOnClick={false} closeOnClick={false}
> >
{children}
</Popup>
);
};
const SingleFeaturePopup = ({ feature, onSelect }) => {
const { include, selection, exclude } = usePointSelection();
const doesMatchFilter = feature.layer.id === "match-points";
const isSelected =
(doesMatchFilter || selection.included.has(feature.properties.id)) &&
!selection.excluded.has(feature.properties.id);
const handleSelect = () => {
if (isSelected) {
exclude(feature.properties.id);
} else {
include(feature.properties.id);
}
onSelect();
};
return (
<>
<div> <div>
{Object.entries(feature.properties).map(([key, value]) => { {popupConfig.map(({ field, name }) => {
return ( return (
<Row className={twMerge("p-1")} key={key}> <Row className={twMerge("p-1")} key={field}>
<Col className={"font-semibold"} span={15}> <Col className={"font-semibold"} span={15}>
{key} {name}
</Col> </Col>
<Col span={9}>{value}</Col> <Col span={9}>{feature.properties[field]}</Col>
</Row> </Row>
); );
})} })}
@ -52,6 +67,56 @@ export const MapPopup = ({ feature, lat, lng, onClose }) => {
<Button type="text" className="mt-2 mx-auto" block onClick={handleSelect}> <Button type="text" className="mt-2 mx-auto" block onClick={handleSelect}>
{isSelected ? "Исключить из выборки" : "Добавить в выборку"} {isSelected ? "Исключить из выборки" : "Добавить в выборку"}
</Button> </Button>
</Popup> </>
);
};
const MultipleFeaturesPopup = ({ features, onSelect }) => {
return (
<div className="space-y-2 p-1">
{features.map((feature) => {
return (
<Button
type={feature.layer.id === "match-points" ? "primary" : "text"}
className="flex items-center gap-x-1"
block
onClick={() => onSelect(feature)}
key={feature.properties.id}
>
<span>{feature.properties.id}</span>
<span>{feature.properties.category}</span>
</Button>
);
})}
</div>
);
};
export const MapPopup = ({ features, lat, lng, onClose }) => {
const [selectedFeature, setSelectedFeature] = useState(null);
const getContent = () => {
if (features.length === 1) {
return <SingleFeaturePopup feature={features[0]} onSelect={onClose} />;
}
if (selectedFeature) {
return (
<SingleFeaturePopup feature={selectedFeature} onSelect={onClose} />
);
}
return (
<MultipleFeaturesPopup
features={features}
onSelect={setSelectedFeature}
/>
);
};
return (
<PopupWrapper lat={lat} lng={lng} onClose={onClose}>
{getContent()}
</PopupWrapper>
); );
}; };

@ -39,6 +39,18 @@ export const pointLayer = {
}, },
}; };
export const unmatchPointLayer = {
type: "circle",
layout: {},
paint: {
"circle-color": "#b4b4b4",
"circle-radius": 3,
"circle-stroke-width": 0.4,
"circle-stroke-color": "#fff",
"circle-opacity": 0.8,
},
};
export const gridLayer = { export const gridLayer = {
type: "fill", type: "fill",
layout: {}, layout: {},

@ -1,6 +1,6 @@
import { Title } from "./Title"; import { Title } from "./Title";
import { Slider } from "antd"; import { Slider } from "antd";
import { useState } from "react"; import { useEffect, useState } from "react";
const Mark = ({ value }) => { const Mark = ({ value }) => {
return <span className={"text-grey text-xs"}>{value}</span>; return <span className={"text-grey text-xs"}>{value}</span>;
@ -42,6 +42,11 @@ export const SliderComponent = ({
getInitialMarks(fullRangeMarks, initialValue) getInitialMarks(fullRangeMarks, initialValue)
); );
useEffect(() => {
setValue(initialValue);
setMarks(getInitialMarks(fullRangeMarks, initialValue));
}, [initialValue]);
const handleAfterChange = (value) => { const handleAfterChange = (value) => {
if (Array.isArray(value)) { if (Array.isArray(value)) {
const [min, max] = value; const [min, max] = value;

@ -1,11 +1,3 @@
export const TYPE_MAPPER = {
kiosk: "Городской киоск",
mfc: "МФЦ",
library: "Библиотека",
dk: "Дом культуры и отдыха",
sport: "Спортивный объект",
};
export const factorsNameMapper = { export const factorsNameMapper = {
people: "Численность населения в 2021 г.", people: "Численность населения в 2021 г.",
people2025: "Численность населения в 2025 г. (прогноз)", people2025: "Численность населения в 2025 г. (прогноз)",

@ -13,7 +13,7 @@ export const LayersVisibility = () => {
onChange={() => toggleVisibility("points")} onChange={() => toggleVisibility("points")}
checked={isVisible.points} checked={isVisible.points}
> >
Точки размещения постаматов Локации к рассмотрению
</Checkbox> </Checkbox>
<Checkbox <Checkbox
className={"!ml-0"} className={"!ml-0"}

@ -10,11 +10,13 @@ import { useFilters } from "../../stores/useFilters";
// sport - спортивные объекты // sport - спортивные объекты
const types = [ const types = [
{ id: "kiosk", name: "Городские киоски" }, "Городской киоск",
{ id: "mfc", name: "МФЦ" }, "МФЦ",
{ id: "library", name: "Библиотеки" }, "Библиотека",
{ id: "dk", name: "Дома культуры и клубы" }, "Спортивный объект",
{ id: "sport", name: "Спортивные объекты" }, "Ритейл",
"Подъезд жилого дома",
"Дом культуры/Клуб",
]; ];
const SelectItem = ({ name, isActive, onClick }) => { const SelectItem = ({ name, isActive, onClick }) => {
@ -58,10 +60,10 @@ export const ObjectTypesSelect = () => {
<div className="space-y-2"> <div className="space-y-2">
{types.map((type) => ( {types.map((type) => (
<SelectItem <SelectItem
key={type.id} key={type}
name={type.name} name={type}
isActive={filters.categories.includes(type.id)} isActive={filters.categories.includes(type)}
onClick={() => handleClick(type.id)} onClick={() => handleClick(type)}
/> />
))} ))}
</div> </div>

@ -1,18 +1,37 @@
import { SliderComponent as Slider } from "../../components/SliderComponent"; import { SliderComponent as Slider } from "../../components/SliderComponent";
import { useFilters } from "../../stores/useFilters"; import { useFilters } from "../../stores/useFilters";
import { useQuery } from "@tanstack/react-query";
import { api } from "../../api";
import { useEffect } from "react";
export const PredictionSlider = () => { export const PredictionSlider = () => {
const { filters, setPrediction } = useFilters(); const { filters, setPrediction } = useFilters();
const { data } = useQuery(["max-min"], async () => {
const { data } = await api.get(`/api/placement_points/filters`);
return data;
});
const handleAfterChange = (range) => setPrediction(range); const handleAfterChange = (range) => setPrediction(range);
useEffect(() => {
if (!data) return;
const min = data.prediction_current[0];
const max = data.prediction_current[1];
setPrediction([min, max]);
}, [data]);
// if (!data) return null;
return ( return (
<Slider <Slider
title={"Прогнозный трафик, чел."} title={"Прогнозный трафик, чел."}
value={filters.prediction} value={filters.prediction}
onAfterChange={handleAfterChange} onAfterChange={handleAfterChange}
min={1} min={200}
max={100} max={299}
range range
/> />
); );

@ -32,8 +32,8 @@ const columns = [
}, },
{ {
title: "Прогноз", title: "Прогноз",
dataIndex: "prediction", dataIndex: "prediction_first",
key: "prediction", key: "prediction_first",
width: "120px", width: "120px",
ellipsis: true, ellipsis: true,
}, },
@ -89,13 +89,23 @@ export const Table = React.memo(({ height = 200 }) => {
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const { filters } = useFilters(); const { filters } = useFilters();
const { prediction, status } = filters;
const { include, selection, exclude } = usePointSelection(); const { include, selection, exclude } = usePointSelection();
const { data } = useQuery( const { data } = useQuery(
["table", page, filters], ["table", page, filters],
async () => { async () => {
const params = new URLSearchParams({
page,
page_size: PAGE_SIZE,
"prediction_current[]": prediction,
"status[]": status,
"included[]": [...selection.included],
"excluded[]": [...selection.excluded],
});
const { data } = await api.get( const { data } = await api.get(
`/api/placement_points?page=${page}&page_size=${PAGE_SIZE}&prediction=${filters.prediction}` `/api/placement_points?${params.toString()}`
); );
return data; return data;

@ -2,9 +2,10 @@ import { create } from "zustand";
import { immer } from "zustand/middleware/immer"; import { immer } from "zustand/middleware/immer";
const INITIAL = { const INITIAL = {
prediction: [50, 100], prediction: [200, 299],
categories: [], categories: [],
region: null, region: null,
status: ["К рассмотрению"],
}; };
const store = (set) => ({ const store = (set) => ({

@ -4,16 +4,13 @@ import { immer } from "zustand/middleware/immer";
const store = (set) => ({ const store = (set) => ({
popup: null, popup: null,
setPopup: (feature) => { setPopup: (popupState) => {
set((state) => { set((state) => {
if (!feature) { if (!popupState) {
state.popup = null; state.popup = null;
return state; return state;
} }
state.popup = { state.popup = popupState;
feature,
coordinates: feature.coordinates,
};
}); });
}, },
}); });

Loading…
Cancel
Save