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

@ -1,10 +1,38 @@
import { Layer, Source } from "react-map-gl";
import { pointLayer } from "./layers-config";
import { pointLayer, unmatchPointLayer } from "./layers-config";
import { useLayersVisibility } from "../stores/useLayersVisibility";
import { BASE_URL } from "../api";
import { useFilters } from "../stores/useFilters";
import { usePointSelection } from "../stores/usePointSelection";
export const Points = () => {
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 (
<>
@ -17,13 +45,26 @@ export const Points = () => {
>
<Layer
{...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-layer={"public.service_placementpoint"}
layout={{
...pointLayer.layout,
visibility: isVisible.points ? "visible" : "none",
}}
filter={matchFilterExpression}
paint={pointLayer.paint}
/>
</Source>

@ -1,34 +1,25 @@
import { Popup } from "react-map-gl";
import { Button, Col, Row } from "antd";
import { twMerge } from "tailwind-merge";
import { TYPE_MAPPER } from "../config";
import { usePointSelection } from "../stores/usePointSelection";
import { useState } from "react";
const pointConfig = [
const popupConfig = [
{
field: "name",
name: "Название",
},
{
field: "category",
name: "Тип",
formatter: (value) => TYPE_MAPPER[value],
},
{
name: "Востребованность, у.е.",
formatter: (value) => Math.round(value),
field: "prediction_current",
name: "Прогнозный трафик",
},
];
export const MapPopup = ({ feature, lat, lng, onClose }) => {
const { include, selection, exclude } = usePointSelection();
const isSelected = selection.included.has(feature.properties.id);
const handleSelect = () =>
isSelected
? exclude(feature.properties.id)
: include(feature.properties.id);
const PopupWrapper = ({ lat, lng, onClose, children }) => {
return (
<Popup
longitude={lng}
@ -37,14 +28,38 @@ export const MapPopup = ({ feature, lat, lng, onClose }) => {
onClose={onClose}
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>
{Object.entries(feature.properties).map(([key, value]) => {
{popupConfig.map(({ field, name }) => {
return (
<Row className={twMerge("p-1")} key={key}>
<Row className={twMerge("p-1")} key={field}>
<Col className={"font-semibold"} span={15}>
{key}
{name}
</Col>
<Col span={9}>{value}</Col>
<Col span={9}>{feature.properties[field]}</Col>
</Row>
);
})}
@ -52,6 +67,56 @@ export const MapPopup = ({ feature, lat, lng, onClose }) => {
<Button type="text" className="mt-2 mx-auto" block onClick={handleSelect}>
{isSelected ? "Исключить из выборки" : "Добавить в выборку"}
</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 = {
type: "fill",
layout: {},

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

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

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

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

@ -1,18 +1,37 @@
import { SliderComponent as Slider } from "../../components/SliderComponent";
import { useFilters } from "../../stores/useFilters";
import { useQuery } from "@tanstack/react-query";
import { api } from "../../api";
import { useEffect } from "react";
export const PredictionSlider = () => {
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);
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 (
<Slider
title={"Прогнозный трафик, чел."}
value={filters.prediction}
onAfterChange={handleAfterChange}
min={1}
max={100}
min={200}
max={299}
range
/>
);

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

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

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

Loading…
Cancel
Save