Files
sgeUpdated/sge-frontend/src/views/Map.js

837 lines
27 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect } from "react";
import {
MapContainer,
Marker,
Polygon,
Popup,
TileLayer,
Tooltip,
useMap,
LayerGroup,
} from "react-leaflet";
import "../components/leaflet.css";
import { useState, useRef } from "react";
import {
Button,
Label,
Modal,
ModalHeader,
ModalBody,
Row,
Col,
Input,
Card,
CardHeader,
CardTitle,
} from "reactstrap";
import Select from "react-select";
import { useTranslation } from "react-i18next";
import DataInputGroup from "../components/data-input/index.js";
import { useSelector, useDispatch } from "react-redux";
import { getCities } from "../redux/actions/cities";
import { v4 as uuidv4 } from "uuid";
import { getCity } from "../redux/actions/city";
import { getDistrict } from "../redux/actions/district";
// import {
// getOrganisations,
// getOrganisationById,
// } from "../redux/actions/organisations";
import { getAreasWithCriteria } from "../redux/actions/areas";
import { ChromePicker } from "react-color";
import { customFilterForSelect } from "../utility/Utils";
import { permissionCheck } from "../components/permission-check";
import { getDataCenters } from "../redux/actions/dataCenter";
import L from "leaflet";
// Custom data center icon
const dataCenterIcon = new L.Icon({
iconUrl:
"data:image/svg+xml;base64," +
btoa(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<defs>
<linearGradient id="serverGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#4A90E2;stop-opacity:1" />
<stop offset="100%" style="stop-color:#2E5BBA;stop-opacity:1" />
</linearGradient>
<linearGradient id="rackGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#F5F5F5;stop-opacity:1" />
<stop offset="100%" style="stop-color:#E0E0E0;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Main server rack -->
<rect x="8" y="4" width="32" height="40" rx="2" ry="2" fill="url(#rackGradient)" stroke="#B0B0B0" stroke-width="1"/>
<!-- Server units -->
<rect x="10" y="6" width="28" height="4" rx="1" fill="url(#serverGradient)" stroke="#2E5BBA" stroke-width="0.5"/>
<rect x="10" y="12" width="28" height="4" rx="1" fill="url(#serverGradient)" stroke="#2E5BBA" stroke-width="0.5"/>
<rect x="10" y="18" width="28" height="4" rx="1" fill="url(#serverGradient)" stroke="#2E5BBA" stroke-width="0.5"/>
<rect x="10" y="24" width="28" height="4" rx="1" fill="url(#serverGradient)" stroke="#2E5BBA" stroke-width="0.5"/>
<rect x="10" y="30" width="28" height="4" rx="1" fill="url(#serverGradient)" stroke="#2E5BBA" stroke-width="0.5"/>
<rect x="10" y="36" width="28" height="4" rx="1" fill="url(#serverGradient)" stroke="#2E5BBA" stroke-width="0.5"/>
<!-- LED indicators -->
<circle cx="13" cy="8" r="0.8" fill="#00FF00"/>
<circle cx="13" cy="14" r="0.8" fill="#00FF00"/>
<circle cx="13" cy="20" r="0.8" fill="#FFFF00"/>
<circle cx="13" cy="26" r="0.8" fill="#00FF00"/>
<circle cx="13" cy="32" r="0.8" fill="#FF0000"/>
<circle cx="13" cy="38" r="0.8" fill="#00FF00"/>
<!-- Power indicators -->
<circle cx="35" cy="8" r="0.6" fill="#0080FF"/>
<circle cx="35" cy="14" r="0.6" fill="#0080FF"/>
<circle cx="35" cy="20" r="0.6" fill="#0080FF"/>
<circle cx="35" cy="26" r="0.6" fill="#0080FF"/>
<circle cx="35" cy="32" r="0.6" fill="#0080FF"/>
<circle cx="35" cy="38" r="0.6" fill="#0080FF"/>
<!-- Ventilation grilles -->
<rect x="16" y="7" width="16" height="0.5" fill="#1A4A8A"/>
<rect x="16" y="8.5" width="16" height="0.5" fill="#1A4A8A"/>
<rect x="16" y="13" width="16" height="0.5" fill="#1A4A8A"/>
<rect x="16" y="14.5" width="16" height="0.5" fill="#1A4A8A"/>
<rect x="16" y="19" width="16" height="0.5" fill="#1A4A8A"/>
<rect x="16" y="20.5" width="16" height="0.5" fill="#1A4A8A"/>
<rect x="16" y="25" width="16" height="0.5" fill="#1A4A8A"/>
<rect x="16" y="26.5" width="16" height="0.5" fill="#1A4A8A"/>
<rect x="16" y="31" width="16" height="0.5" fill="#1A4A8A"/>
<rect x="16" y="32.5" width="16" height="0.5" fill="#1A4A8A"/>
<rect x="16" y="37" width="16" height="0.5" fill="#1A4A8A"/>
<rect x="16" y="38.5" width="16" height="0.5" fill="#1A4A8A"/>
<!-- Base/feet -->
<rect x="6" y="42" width="4" height="2" rx="1" fill="#808080"/>
<rect x="38" y="42" width="4" height="2" rx="1" fill="#808080"/>
<!-- Shadow -->
<ellipse cx="24" cy="45" rx="18" ry="2" fill="#000000" opacity="0.2"/>
</svg>
`),
iconSize: [48, 48],
iconAnchor: [18, 36],
popupAnchor: [0, -36],
});
const ColorPicker = ({ selectedColors, setSelectedColors, index }) => {
const [showColorPicker, setShowColorPicker] = useState(false);
useEffect(() => {
const storedColors = localStorage.getItem("selectedMapColors");
if (storedColors) {
setSelectedColors(JSON.parse(storedColors));
}
}, []);
useEffect(() => {
localStorage.setItem("selectedMapColors", JSON.stringify(selectedColors));
}, [selectedColors]);
const handleColorChange = (color) => {
const rgbaColor = `rgba(${color.rgb.r}, ${color.rgb.g}, ${color.rgb.b}, 0.3)`;
const updatedColors = [...selectedColors];
updatedColors[index] = rgbaColor;
setSelectedColors(updatedColors);
};
const handlePickerOutsideClick = (event) => {
// Renk seçici dışına tıklama kontrolü
if (
event.target.closest(".color-picker-container") === null &&
event.target.closest(".color-box") === null
) {
setShowColorPicker(false);
}
};
useEffect(() => {
// Tüm belgeye tıklama olayını dinle
document.addEventListener("click", handlePickerOutsideClick);
return () => {
// Bileşen kaldırıldığında tıklama olayının dinlemesini kaldır
document.removeEventListener("click", handlePickerOutsideClick);
};
}, []);
return (
<div className="color-picker-container">
<div
key={index}
className="color-box"
style={{
backgroundColor: selectedColors[index],
width: "30px",
height: "15px",
display: "inline-block",
marginRight: "10px",
cursor: "pointer",
}}
onClick={() => setShowColorPicker(true)}
></div>
{showColorPicker && (
<div style={{ position: "absolute", zIndex: 1000 }}>
<ChromePicker
color={selectedColors[index]}
onChange={handleColorChange}
/>
</div>
)}
</div>
);
};
const Map = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
const citiesStore = useSelector((state) => state.cities);
const cityStore = useSelector((state) => state.city);
const districtStore = useSelector((state) => state.district);
// const OrganisationsStore = useSelector((state) => state.organizations);
const areasStore = useSelector((state) => state.areas);
const dataCenterStore = useSelector((state) => state.dataCenter);
const [cities, setCities] = useState([]);
const [districts, setDistricts] = useState([]);
const [neighborhoods, setNeighborhoods] = useState([]);
// const [organizationOptions, setOrganizationOptions] = useState([]);
// const organizationId = localStorage.getItem("organizationId");
// const roleTag = localStorage.getItem("roleTag");
// const [selectedOrganization, setSelectedOrganization] = useState({
// label: localStorage.getItem("organizationName"),
// value: organizationId,
// });
const [areasOptions, setAreasOptions] = useState([]);
const [done, setDone] = useState(false);
const [selectedCity, setSelectedCity] = useState(null);
const [selectedDistrict, setSelectedDistrict] = useState(null);
const [districtView, setDistrictView] = useState(false);
const [neighbourhoodView, setNeighbourhoodView] = useState(false);
const cityRefs = useRef({});
const districtRefs = useRef({});
const neighbourhoodRefs = useRef({});
const [zoom, setZoom] = useState(6.7);
const [center, setCenter] = useState({ lat: 38.5, lng: 35.27 });
const [showDataInputModal, setShowDataInputModal] = useState(false);
const [inputData, setInputData] = useState({
// organization: selectedOrganization,
});
const [referance, setReferance] = useState(
Number(localStorage.getItem("referance")) || 1000
);
const [referance2, setReferance2] = useState(
Number(localStorage.getItem("referance2")) || 2000
);
const initialColors = [
"rgba(44,232,44,0.3)",
"rgba(234,234,17,0.3)",
"rgba(228,19,19,0.3)",
];
const [selectedColors, setSelectedColors] = useState(initialColors);
const currentYear = new Date().getFullYear();
const [year, setYear] = useState({ value: currentYear, label: currentYear });
const renderYearOptions = () => {
const years = [];
for (let year = currentYear; year >= 1990; year--) {
years.push({ label: year, value: year });
}
return years;
};
useEffect(() => {
dispatch(getCities());
dispatch(getDataCenters());
}, []);
// useEffect(() => {
// if (selectedOrganization?.value != "undefined") {
// dispatch(getAreasWithCriteria(selectedOrganization.value));
// }
// }, [selectedOrganization]);
useEffect(() => {
setAreasOptions([]);
// Load all areas without organization filter for now
// You may want to add organization filtering back later if needed
const citiesOptions =
areasStore?.areasWithCriteria
?.map((area) =>
Array.isArray(area?.cities) // cities'in bir dizi olup olmadığını kontrol edin
? area.cities.map((city) => ({
value: city?.id,
label: city?.name,
type: "city",
}))
: []
)
.flat() || []; // Eğer map boş dönerse, `flat` sonrası boş bir dizi kullan
const districtsOptions = areasStore?.areasWithCriteria
?.map((area) => {
return area?.districts?.map((district) => {
return {
value: district?.id,
label: district?.name + " (" + district?.city?.name + ")",
type: "district",
};
});
})
.flat();
const neighborhoodsOptions = areasStore?.areasWithCriteria
?.map((area) => {
return area?.neighborhoods?.map((neighborhood) => {
return {
value: neighborhood?.id,
label:
neighborhood?.name +
" (" +
neighborhood?.district?.name +
" / " +
neighborhood?.district?.city?.name +
")",
type: "neighborhood",
};
});
})
.flat();
setAreasOptions([
...(citiesOptions || []),
...(districtsOptions || []),
...(neighborhoodsOptions || []),
]);
}, [areasStore]);
// useEffect(() => {
// if (roleTag === "SUPER_ADMIN") {
// dispatch(getOrganisations());
// } else {
// dispatch(getOrganisationById(organizationId));
// }
// }, []);
const handleDataInputButtonPressed = ({ area, type }) => {
const areaName =
type === "neighborhood"
? `${area?.name} / ${area?.district?.name} / ${area?.district?.city?.name}`
: type === "district"
? `${area?.name} / ${area?.city?.name}`
: area?.name;
const areaType =
type === "district"
? "district"
: type === "city"
? "city"
: type === "neighborhood"
? "neighborhood"
: "";
setShowDataInputModal(true);
setInputData({
...inputData,
area: { value: area?.id, label: areaName, type: areaType },
});
};
useEffect(() => {
if (!citiesStore?.cities || !year?.value) {
console.error("Cities or year is not defined.");
return;
}
setCities(() => {
const updatedCities = citiesStore.cities.map((city) => {
const mainDataTables = city.mainDataTables
? city.mainDataTables.filter(
(data) => data.year === year.value.toString()
)
: [];
const total = mainDataTables.reduce(
(acc, data) => acc + (data?.totalEmission || 0),
0
);
return { ...city, mainDataTables, total };
});
return updatedCities;
});
}, [citiesStore?.cities, year?.value]);
useEffect(() => {
if (selectedCity != undefined) {
dispatch(getCity(selectedCity?.id));
}
}, [selectedCity]);
useEffect(() => {
if (selectedDistrict != undefined && selectedDistrict.length != 0) {
dispatch(getDistrict(selectedDistrict?.id));
}
}, [selectedDistrict]);
useEffect(() => {
if (!cityStore?.city?.districts || !year.value) {
return;
}
setDistricts(() => {
const updatedDistricts = cityStore.city.districts?.map((district) => {
const mainDataTables = district.mainDataTables.filter(
(data) => data.year === year.value.toString()
);
const total = mainDataTables.reduce(
(acc, data) => acc + data.totalEmission,
0
);
return { ...district, mainDataTables, total };
});
return updatedDistricts;
});
}, [selectedCity, cityStore?.city, year.value]);
useEffect(() => {
if (!districtStore?.district?.neighborhoods || !year.value) {
return;
}
setNeighborhoods(() => {
const updatedNeighborhoods = districtStore.district?.neighborhoods?.map(
(neighborhood) => {
const mainDataTables = neighborhood.mainDataTables.filter(
(data) => data.year === year.value.toString()
);
const total = mainDataTables.reduce(
(acc, data) => acc + data.totalEmission,
0
);
return { ...neighborhood, mainDataTables, total };
}
);
return updatedNeighborhoods;
});
}, [selectedDistrict, districtStore?.district, year.value]);
// useEffect(() => {
// let organizationOptions = [];
// if (
// OrganisationsStore.organization &&
// OrganisationsStore.organization.length !== 0
// ) {
// organizationOptions.push({
// value: OrganisationsStore.organization.id,
// label: OrganisationsStore.organization.tag,
// });
// if (OrganisationsStore.organization.children) {
// organizationOptions = [
// ...organizationOptions,
// ...OrganisationsStore.organization.children.map((organization) => ({
// value: organization.child.id,
// label: organization.child.tag,
// })),
// ];
// }
// } else {
// organizationOptions = OrganisationsStore.dataOrganization.map(
// (organization) => ({
// value: organization.id,
// label: organization.tag,
// })
// );
// }
// setOrganizationOptions(organizationOptions);
// }, [OrganisationsStore]);
const renderDataInputModal = () => {
return (
<Modal
isOpen={showDataInputModal}
toggle={() => setShowDataInputModal(!showDataInputModal)}
className="modal-dialog-centered"
size="lg"
>
<ModalHeader toggle={() => setShowDataInputModal(!showDataInputModal)}>
{t("DataInput.dataInput")} - {inputData?.area?.label}
</ModalHeader>
<ModalBody>
<DataInputGroup inputData={inputData} setInputData={setInputData} />
</ModalBody>
</Modal>
);
};
const grayOptions = {
color: "gray",
fillColor: "transparent",
opacity: 1,
};
const greenOptions = {
color: "gray",
fillColor: selectedColors[0],
opacity: 1,
};
const yellowOptions = {
color: "gray",
fillColor: selectedColors[1],
opacity: 1,
};
const redOptions = {
color: "gray",
fillColor: selectedColors[2],
opacity: 1,
};
const pathOptions = (total) => {
return total > referance2
? redOptions
: referance < total && total < referance2
? yellowOptions
: total != 0 && total < referance
? greenOptions
: grayOptions;
};
const turkeyBounds = [
[34.42, 44.83],
[43.45, 25.62],
];
const RenderDistrictView = () => {
return districts?.map((district, index) => {
return (
<Polygon
key={uuidv4()}
ref={(c) => {
districtRefs.current[index] = c;
if (index === district.length - 1 && !done) {
setDone(true);
}
}}
fillOpacity={1}
pathOptions={pathOptions(district.total)}
positions={convertCoordinates(district.coordinates)}
eventHandlers={{
mouseover: () => {
const district = districtRefs.current[index];
district.openPopup();
},
}}
>
<Tooltip permanent direction="center" className="tooltip">
{district.name}
</Tooltip>
{
<Popup closeButton={true} autoPan={false}>
<h5 className="w-100 text-center">{district.name}</h5>
<Button
className="w-100 mb-1"
color="primary"
onClick={() => {
setSelectedDistrict(district);
setNeighbourhoodView(true);
setDistrictView(false);
setSelectedCity(null);
setZoom(11.0);
let convertCordinates = convertCoordinates(
district.coordinates
);
let length = convertCordinates[0][0][0].length;
let mlength = ((length + 1) / 2).toFixed(0);
let lat1 = convertCordinates[0][0][0][0];
let lng1 = convertCordinates[0][0][0][1];
let lat2 = convertCordinates[0][0][mlength][0];
let lng2 = convertCordinates[0][0][mlength][1];
setCenter({
lat: ((lat1 + lat2) / 2).toFixed(2),
lng: ((lng1 + lng2) / 2).toFixed(2),
});
}}
>
{t("Areas.neighborhoods")}
</Button>
<br></br>
{areaCheck({ areaId: district.id }) &&
permissionCheck("dataset_create") && (
<Button
className="w-100 mb-1"
color="primary"
onClick={() =>
handleDataInputButtonPressed({
area: district,
type: "district",
})
}
>
{t("DataInput.dataInput")}
</Button>
)}
<br></br>
<Button
className="w-100"
color="primary"
onClick={() => {
setDistrictView(false);
setCenter({ lat: 38, lng: 37 });
setZoom(6.7);
}}
>
{t("Map.goBack")}
</Button>
</Popup>
}
</Polygon>
);
});
};
const neighborhoodTooltipBackground = (total) => {
return total > referance2
? selectedColors[2]
: referance < total && total < referance2
? selectedColors[1]
: total != 0 && total < referance
? selectedColors[0]
: "white";
};
const renderNeighbourhoodView = () => {
return neighborhoods?.map((neighbourhood, index) => {
return (
<Marker
opacity={0}
zoom={10.75}
key={uuidv4()}
ref={(c) => {
// <--- add city refs to ref object here
neighbourhoodRefs.current[index] = c;
if (index === neighbourhood.length - 1 && !done) {
setDone(true);
}
}}
fillOpacity={1}
pathOptions={pathOptions(neighbourhood.total)}
position={[
Number(neighbourhood?.minLat + neighbourhood?.maxLat) / 2,
Number(neighbourhood?.minLong + neighbourhood?.maxLong) / 2,
]}
eventHandlers={{
mouseover: () => {
const neighbourhood = neighbourhoodRefs.current[index];
neighbourhood.openPopup();
},
}}
>
<Tooltip permanent direction="center" className="tooltip">
{neighbourhood.name}
</Tooltip>
<Popup closeButton={true} autoPan={false}>
{areaCheck({ areaId: neighbourhood.id }) &&
permissionCheck("dataset_create") && (
<Button
className="w-100 mb-1"
color="primary"
onClick={() =>
handleDataInputButtonPressed({
area: neighbourhood,
type: "neighborhood",
})
}
>
{t("DataInput.dataInput")}
</Button>
)}
<br></br>
<Button
className="w-100"
color="primary"
onClick={() => {
setNeighbourhoodView(false);
setCenter({ lat: 38, lng: 37 });
setZoom(6.7);
}}
>
{t("Map.goBack")}
</Button>
</Popup>
</Marker>
);
});
};
function ChangeZoom({ center, zoom }) {
const map = useMap();
map.setView(center, zoom);
return null;
}
function convertCoordinates(coords) {
const editedCoordinates = JSON.parse(coords);
return editedCoordinates;
}
function areaCheck({ areaId }) {
return areasOptions?.some((area) => areaId === area.value);
}
const renderDataCenterMarkers = () => {
if (!dataCenterStore?.dataCenters) return null;
return (
<LayerGroup>
{dataCenterStore.dataCenters.map((dc) => {
if (!dc.latitude || !dc.longitude) return null;
return (
<Marker
key={dc.id}
position={[dc.latitude, dc.longitude]}
icon={dataCenterIcon}
>
<Popup>
<div className="data-center-popup">
<h5 className="mb-2 text-primary">{dc.dataCenter}</h5>
<div className="mb-2">
<p className="mb-1">
<strong>{t("DataCenter.city")}:</strong>{" "}
<span>{dc.city?.name || "-"}</span>
</p>
{dc.address && (
<p className="mb-1 small text-muted">
<strong>{t("Address")}:</strong> {dc.address}
</p>
)}
</div>
<p className="mb-1">
<strong>{t("DataCenter.number")}:</strong> {dc.number}
</p>
<p className="mb-1">
<strong>{t("DataCenter.externalId")}:</strong>{" "}
{dc.externalId}
</p>
{dc.area && (
<p className="mb-1">
<strong>{t("Area")}:</strong> {dc.area.tag}
</p>
)}
{dc.physicalMachines?.length > 0 && (
<p className="mb-1">
<strong>{t("Physical Machines")}:</strong>{" "}
<span className="badge badge-secondary">
{dc.physicalMachines.length}
</span>
</p>
)}
{dc.dataCenterEmissionSources?.length > 0 && (
<p className="mb-1">
<strong>{t("EmissionSources.emissionSources")}:</strong>{" "}
<span className="badge badge-info">
{dc.dataCenterEmissionSources.length}
</span>
</p>
)}
{dc.ayposURL && (
<Button
className="w-100 mb-1"
color="primary"
onClick={() => window.open(dc.ayposURL, "_blank")}
>
Dashboard
</Button>
)}
</div>
</Popup>
<Tooltip>{dc.dataCenter}</Tooltip>
</Marker>
);
})}
</LayerGroup>
);
};
return (
<Card>
<CardHeader className="border-bottom">
<CardTitle tag="h4">{t("Map.title")}</CardTitle>
</CardHeader>
<div style={{ height: "700px", width: "100%" }}>
<MapContainer
center={center}
zoom={zoom}
style={{ height: "100%", width: "100%" }}
>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
/>
{renderDataCenterMarkers()}
{neighbourhoodView
? renderNeighbourhoodView(selectedDistrict)
: districtView
? RenderDistrictView(selectedCity)
: cities.map((city, index) => {
return (
<Polygon
key={uuidv4()}
ref={(c) => {
cityRefs.current[index] = c;
if (index === cities.length - 1 && !done) {
setDone(true);
}
}}
fillOpacity={1}
pathOptions={pathOptions(city.total)}
positions={convertCoordinates(city.coordinates)}
eventHandlers={{
click: () => {
setSelectedCity(city);
setDistrictView(true);
setZoom(8.0);
let convertCordinates = convertCoordinates(
city.coordinates
);
let length = convertCordinates[0][0][0].length;
let mlength = ((length + 1) / 2).toFixed(0);
let lat1 = convertCordinates[0][0][0][0];
let lng1 = convertCordinates[0][0][0][1];
let lat2 = convertCordinates[0][0][mlength][0];
let lng2 = convertCordinates[0][0][mlength][1];
setCenter({
lat: ((lat1 + lat2) / 2).toFixed(2),
lng: ((lng1 + lng2) / 2).toFixed(2),
});
},
}}
>
<Tooltip permanent direction="center">
{city.name}
</Tooltip>
</Polygon>
);
})}
{renderDataInputModal()}
</MapContainer>
</div>
</Card>
);
};
export default Map;