You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

734 lines
29 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import { Dialog, Disclosure, Menu, Transition } from "@headlessui/react";
import {
ChevronDownIcon,
ChevronUpIcon,
} from "@heroicons/react/20/solid";
import { XMarkIcon } from "@heroicons/react/24/outline";
import { Fragment, useRef, useState } from "react";
import Map, { Layer, Popup, ScaleControl, Source } from "react-map-gl/maplibre";
import basemap from "./assets/basemap.json";
import mapstyle from "./countries.json";
mapstyle.sources.remap.url = import.meta.env.MODE === "localhost" ? "http://localhost:8080/capabilities/remap.json" : mapstyle.sources.remap.url
const paintedLayers = mapstyle.layers.reduce((acc, cur) => ({ ...acc, [cur.id]: cur }), {})
const toggleLayers = ["Подложка"]
const initialPaintFilters = [
{
id: "parameter_name",
name: "Показатель ВИЭ-генерации",
options: [
{
"value": "power_fact_mw",
"label": "Фактическая установленная мощность, МВт",
"checked": true
},
{
"value": "generation_gwh",
"label": "Производство электроэнергии, млн кВт⋅ч",
"checked": false
},
{
"value": "res_share_generation",
"label": "Доля в выработке электроэнергии, %",
"checked": false
},
{
"value": "res_share_power",
"label": "Доля в балансе мощности, %",
"checked": false
},
{
"value": "generation_to_population_kWh_pers",
"label": "Производство электроэнергии на душу населения, кВт⋅ч/чел.",
"checked": false
}
]
},
]
const initialFilterFilters = [
{
id: "id_tech",
name: "ВИЭ-технология",
options: [
{
"value": 1,
"label": "СЭС",
"checked": false
},
{
"value": 2,
"label": "Наземные ВЭС",
"checked": false
},
{
"value": 3,
"label": "Офшорные ВЭС",
"checked": false
},
{
"value": 4,
"label": "ГЭС",
"checked": false
},
{
"value": 5,
"label": "БиоЭС",
"checked": false
},
{
"value": 6,
"label": "ГеоЭС",
"checked": false
},
{
"value": 8,
"label": "СЭС и ВЭС",
"checked": false
},
{
"value": 7,
"label": "Все ВИЭ",
"checked": true
}
]
},
{
"id": "year",
"name": "Год",
"options": [
{
"value": 2010,
"label": 2010,
"checked": false
},
{
"value": 2011,
"label": 2011,
"checked": false
},
{
"value": 2012,
"label": 2012,
"checked": false
},
{
"value": 2013,
"label": 2013,
"checked": false
},
{
"value": 2014,
"label": 2014,
"checked": false
},
{
"value": 2015,
"label": 2015,
"checked": false
},
{
"value": 2016,
"label": 2016,
"checked": false
},
{
"value": 2017,
"label": 2017,
"checked": false
},
{
"value": 2018,
"label": 2018,
"checked": false
},
{
"value": 2019,
"label": 2019,
"checked": false
},
{
"value": 2020,
"label": 2020,
"checked": false
},
{
"value": 2021,
"label": 2021,
"checked": false
},
{
"value": 2022,
"label": 2022,
"checked": false
},
{
"value": 2023,
"label": 2023,
"checked": false
},
{
"value": 2024,
"label": 2024,
"checked": true
}
]
}
]
export default function Countries({ mapOptions, onMapOptionClick }) {
const mapRef = useRef(null);
const [open, setOpen] = useState(false)
const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false);
const [popupInfo, setPopupInfo] = useState(null);
const [paintFilters, setPaintFilters] = useState(initialPaintFilters);
const [filterFilters, setFilterFilters] = useState(initialFilterFilters);
const generatePopup = (info) => {
// console.log(info)
const [currentTech, currentYear, currentParameter] = filterFilters.map(f => f.options.find(o => o.checked)).concat(paintFilters[0].options.find(o => o.checked))
return (
<div>
<img src={`/flags/${info.id_country}.png`} className="mb-4 w-36 outline outline-gray-400" />
{info.area_km2 && <p className="text-gray-500">Площадь <span className="text-gray-900 font-medium">{(info.area_km2 / 1000).toFixed(1).replace(".", ",")} тыс. км²</span></p>}
{info.population_person && <p className="text-gray-500">Численность населения <span className="text-gray-900 font-medium">{(info.population_person / 1000).toFixed(1).replace(".", ",")} тыс. чел.</span></p>}
{info.population_densitiy_pers_km2 && <p className="text-gray-500">Плотность населения <span className="text-gray-900 font-medium">{info.population_densitiy_pers_km2.toFixed(1).replace(".", ",")} чел./км²</span></p>}
{info.year_carbon_neutrality && <p className="text-gray-500">Целевой год достижения углеродной нейтральности <span className="text-gray-900 font-medium">{info.year_carbon_neutrality}</span></p>}
{info[currentParameter.value] &&
<div className="relative overflow-x-auto py-6">
<p className="text-sm font-medium">{currentParameter.label} в {currentYear.label} году</p>
<table className="w-full text-sm text-left text-gray-500 dark:text-gray-400">
<tbody>
{info[currentParameter.value] != null &&
<tr className="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
<th scope="row" className="px-2 py-4 font-normal text-gray-900 dark:text-white">
{currentTech.label}
</th>
<td className="px-2 py-4 text-right">
{info[currentParameter.value].toFixed(2).replace(".", ",")}
</td>
</tr>
}
</tbody>
</table>
</div>
}
<div className="relative overflow-x-auto pt-2">
<p className="text-sm font-medium">Совокупная установленная мощность ВИЭ по итогам {info.static_date_power} года, МВт</p>
<table className="w-full text-sm text-left text-gray-500 dark:text-gray-400">
<tbody>
{info.static_power_fact_solar_mw != null &&
<tr className="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
<th scope="row" className="px-2 py-4 font-normal text-gray-900 dark:text-white">
Солнечные электростанции
</th>
<td className="px-2 py-4 text-right">
{info.static_power_fact_solar_mw.toFixed(2).replace(".", ",")}
</td>
</tr>
}
{info.static_power_fact_onshore_mw != null &&
<tr className="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
<th scope="row" className="px-2 py-4 font-normal text-gray-900 dark:text-white">
Наземные ветроэлектростанции
</th>
<td className="px-2 py-4 text-right">
{info.static_power_fact_onshore_mw.toFixed(2).replace(".", ",")}
</td>
</tr>
}
{info.static_power_fact_offshore_mw != null &&
<tr className="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
<th scope="row" className="px-2 py-4 font-normal text-gray-900 dark:text-white">
Офшорные ветроэлектростанции
</th>
<td className="px-2 py-4 text-right">
{info.static_power_fact_offshore_mw.toFixed(2).replace(".", ",")}
</td>
</tr>
}
{info.static_power_fact_res_mw != null &&
<tr className="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
<th scope="row" className="px-2 py-4 font-medium text-gray-900 dark:text-white uppercase">
Всего ВИЭ <span className="font-normal normal-case">(включая СЭС, ВЭС, ГЭС, БиоЭС, ГеоЭС)</span>
</th>
<td className="px-2 py-4 text-right">
{info.static_power_fact_res_mw.toFixed(2).replace(".", ",")}
</td>
</tr>
}
{info.static_res_share_power != null &&
<tr className="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
<th scope="row" className="px-2 py-4 font-normal text-gray-900 dark:text-white">
Доля всей ВИЭ-генерации в балансе мощности страны, %
</th>
<td className="px-2 py-4 text-right">
{info.static_res_share_power.toFixed(2).replace(".", ",")}
</td>
</tr>
}
{info.static_solar_wind_share_power != null &&
<tr className="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
<th scope="row" className="px-2 py-4 font-normal text-gray-900 dark:text-white">
Доля СЭС и ВЭС в балансе мощности страны, %
</th>
<td className="px-2 py-4 text-right">
{info.static_solar_wind_share_power.toFixed(2).replace(".", ",")}
</td>
</tr>
}
</tbody>
</table>
</div>
<p className="text-sm text-gray-400">Источник: IRENA</p>
{info.date_actualization && <p className="text-sm mt-4 text-gray-600">Дата актуализации: {info.date_actualization}</p>}
</div>
)
}
const generateLegend = function (parameter) {
const colors_and_breaks = mapstyle.layers.find(layer => layer.id == parameter.value).paint["fill-color"].slice(2)
const colors = colors_and_breaks.filter((cb, i) => i % 2 == 0)
const breaks = colors_and_breaks.filter((cb, i) => i % 2 == 1)
const labels = [`менее ${breaks[0]}`, ...breaks.map((brk, i, arr) => typeof arr[i + 1] !== 'undefined' ? `${brk} - ${arr[i + 1]}` : `более ${breaks.at(-1)}`)]
const legend_items = colors.map((col, i) => (
{
"color": col,
"label": labels[i]
}
)).reverse()
return (
<div>
<p>{parameter.label}</p>
{legend_items.map(item => <div className="flex items-center mt-3" key={item.color}><div className={"w-5 h-5 rounded-sm"} style={{ backgroundColor: item.color }}></div><p className="ml-3 text-sm text-gray-600">{item.label}</p></div>)}
</div>
)
}
const handleFilterToggle = function (event) {
if (paintFilters.filter((filter) => filter.id == event.target.name).length > 0) {
const newStatePaintFilters = paintFilters.map((filter) =>
filter.id == event.target.name
? {
...filter,
options: filter.options.map((option) =>
option.value == event.target.defaultValue
? { ...option, checked: true }
: { ...option, checked: false }
),
}
: filter
);
setPaintFilters(newStatePaintFilters);
} else {
const newStateFilterFilters = filterFilters.map((filter) =>
filter.id == event.target.name
? {
...filter,
options: filter.options.map((option) =>
option.value == event.target.defaultValue
? { ...option, checked: true }
: { ...option, checked: false }
),
}
: filter
);
setFilterFilters(newStateFilterFilters);
}
};
const applyPaintFilters = (filtersObj) => {
const parameter = filtersObj.find(filter => filter.id == "parameter_name").options.find(option => option.checked).value
const newPaint = paintedLayers[parameter].paint
return newPaint
};
const applyFilterFilters = (filtersObj) => {
const year_value = filtersObj.find(f => f.id == "year").options.find(option => option.checked).value
const tech_value = filtersObj.find(f => f.id == "id_tech").options.find(option => option.checked).value
return ["all", ["==", ["get", "year"], year_value], ["==", ["get", "id_tech_global"], tech_value]]
};
const handleClick = (event) => {
if (event.features[0]) {
event.originalEvent.stopPropagation();
setPopupInfo({
lon: event.lngLat.lng,
lat: event.lngLat.lat,
...event.features[0].properties
})
setOpen(true)
}
};
const handleMouseEnter = (event) => {
const feature = event.features[0];
if (!feature) return;
mapRef.current.getCanvas().style.cursor = "pointer";
};
const handleMouseLeave = (event) => {
const feature = event.features[0];
if (!feature) {
return;
}
mapRef.current.getCanvas().style.cursor = "";
};
return (
<div className="bg-white">
<div>
<main className="mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-baseline justify-between border-b border-gray-200 pt-24 pb-6">
<h1 className="text-4xl font-bold tracking-tight text-gray-900">
Развитие возобновляемой энергетики в странах мира
</h1>
<div className="flex items-center">
<Menu as="div" className="relative inline-block text-left">
<div>
<Menu.Button className="hidden group inline-flex justify-center text-sm font-medium text-gray-700 hover:text-gray-900">
{mapOptions.find((option) => option.current).name}
<ChevronDownIcon
className="-mr-1 ml-1 h-5 w-5 flex-shrink-0 text-gray-400 group-hover:text-gray-500"
aria-hidden="true"
/>
</Menu.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
show={true}
>
<Menu.Items className="absolute right-0 z-10 mt-2 w-40 origin-top-right rounded-md bg-white shadow-2xl ring-1 ring-black ring-opacity-5 focus:outline-none">
<div className="py-1">
{mapOptions.map((option) => (
<Menu.Item key={option.name}>
<a
href="#"
className={option.current ? 'font-medium text-gray-900 block px-4 py-2 text-sm' : 'text-gray-500 block px-4 py-2 text-sm'}
onClick={onMapOptionClick}
>
{option.name}
</a>
</Menu.Item>
))}
</div>
</Menu.Items>
</Transition>
</Menu>
</div>
</div>
<section aria-labelledby="products-heading" className="pt-6 pb-24">
<div className="grid grid-cols-1 gap-x-8 gap-y-10 md:grid-cols-4">
<form className="hidden md:block h-[65vh] px-1 py-1 overflow-y-scroll">
{/* Layers */}
<ul
role="list"
className="space-y-4 border-b border-gray-200 pb-6 text-sm font-medium text-gray-900"
>
{toggleLayers.map((layer_name) => (
<div key={layer_name} className="flex items-center">
<input
id={layer_name}
name={layer_name}
defaultValue={layer_name}
type="checkbox"
defaultChecked={true}
className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
onChange={(event) => {
mapRef.current
.getMap()
.setLayoutProperty(
event.target.id,
"visibility",
event.target.checked ? "visible" : "none"
);
}}
/>
<label
htmlFor={layer_name}
className="ml-3 text-sm text-gray-600"
>
{layer_name}
</label>
</div>
))}
</ul>
{/* Legend */}
<Disclosure as="div" className="border-b border-gray-200 py-6">
{({ open }) => (
<>
<h3 className="-my-3 flow-root">
<Disclosure.Button className="flex w-full items-center justify-between bg-white py-3 text-sm text-gray-400 hover:text-gray-500">
<span className="font-medium text-gray-900">
Легенда
</span>
<span className="ml-6 flex items-center">
{open ? (
<ChevronUpIcon
className="h-5 w-5"
aria-hidden="true"
/>
) : (
<ChevronDownIcon
className="h-5 w-5"
aria-hidden="true"
/>
)}
</span>
</Disclosure.Button>
</h3>
<Disclosure.Panel className="pt-6">
<div className="space-y-4">
{generateLegend(paintFilters.find(f => f.id == "parameter_name").options.find(parameter => parameter.checked))}
</div>
</Disclosure.Panel>
</>
)}
</Disclosure>
{/* Filters */}
{paintFilters.concat(filterFilters).map((section) => (
<Disclosure
as="div"
key={section.id}
className="border-b border-gray-200 py-6"
>
{({ open }) => (
<>
<h3 className="-my-3 flow-root">
<Disclosure.Button className="flex w-full items-center justify-between bg-white py-3 text-sm text-gray-400 hover:text-gray-500">
<span className="font-medium text-gray-900">
{section.name}
</span>
<span className="ml-6 flex items-center">
{open ? (
<ChevronUpIcon
className="h-5 w-5"
aria-hidden="true"
/>
) : (
<ChevronDownIcon
className="h-5 w-5"
aria-hidden="true"
/>
)}
</span>
</Disclosure.Button>
</h3>
<Disclosure.Panel className="pt-6">
<div className="space-y-4">
{section.options.map((option, optionIdx) => (
<div
key={option.value}
className="flex items-center"
>
<input
id={`filter-${section.id}-${optionIdx}`}
name={`${section.id}`}
defaultValue={option.value}
type="radio"
defaultChecked={option.checked}
className="peer h-4 w-4 rounded-full border-gray-400 text-indigo-600 focus:ring-indigo-500 disabled:text-gray-400 disabled:border-gray-200"
onChange={handleFilterToggle}
disabled={
(paintFilters[0].options.find(o => o.checked).value == "res_share_generation" ||
paintFilters[0].options.find(o => o.checked).value == "res_share_power" ||
paintFilters[0].options.find(o => o.checked).value == "generation_to_population_kWh_pers") &&
section.id == "id_tech" && option.value < 7 ?
true
: false
}
/>
<label
htmlFor={`filter-${section.id}-${optionIdx}`}
className="ml-3 text-sm text-gray-600 peer-disabled:text-gray-300"
>
{option.label}
</label>
</div>
))}
</div>
</Disclosure.Panel>
</>
)}
</Disclosure>
))}
{/* Metadata */}
<Disclosure as="div" className="border-b border-gray-200 py-6">
{({ open }) => (
<>
<h3 className="-my-3 flow-root">
<Disclosure.Button className="flex w-full items-center justify-between bg-white py-3 text-sm text-gray-400 hover:text-gray-500">
<span className="font-medium text-gray-900">
Метаданные
</span>
<span className="ml-6 flex items-center">
{open ? (
<ChevronUpIcon
className="h-5 w-5"
aria-hidden="true"
/>
) : (
<ChevronDownIcon
className="h-5 w-5"
aria-hidden="true"
/>
)}
</span>
</Disclosure.Button>
</h3>
<Disclosure.Panel className="pt-6">
<div className="space-y-4">
<p className="text-sm">
Границы показаны по состоянию на 2021 год
</p>
<p className="text-sm">
&copy; Ассоциация развития возобновляемой энергетики
</p>
</div>
</Disclosure.Panel>
</>
)}
</Disclosure>
</form>
{/* Map itself */}
<div className="md:col-span-3">
<div className="h-[65vh]">
<Map
initialViewState={{
latitude: mapstyle.center[0],
longitude: mapstyle.center[1],
zoom: mapstyle.zoom,
}}
mapStyle={basemap}
minZoom={mapstyle.minZoom}
ref={mapRef}
interactiveLayerIds={["power_fact_mw"]}
onClick={handleClick}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
maxZoom={6}
hash
>
{open && <Popup
longitude={popupInfo.lon}
latitude={popupInfo.lat}
closeButton={false}
>
{popupInfo.country_russian_name}
</Popup>}
<ScaleControl />
<Source
id="remap"
{...mapstyle.sources.remap}
>
<Layer {...paintedLayers["power_fact_mw"]} paint={applyPaintFilters(paintFilters)} filter={applyFilterFilters(filterFilters)} />
<Layer {...paintedLayers["Границы"]} />
</Source>
</Map>
</div>
</div>
</div>
</section>
</main>
<Transition.Root show={open} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={setOpen}>
<Transition.Child
as={Fragment}
enter="ease-in-out duration-500"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in-out duration-500"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 overflow-hidden">
<div className="absolute inset-0 overflow-hidden">
<div className="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10">
<Transition.Child
as={Fragment}
enter="transform transition ease-in-out duration-500 sm:duration-700"
enterFrom="translate-x-full"
enterTo="translate-x-0"
leave="transform transition ease-in-out duration-500 sm:duration-700"
leaveFrom="translate-x-0"
leaveTo="translate-x-full"
>
<Dialog.Panel className="pointer-events-auto relative w-screen max-w-md">
<Transition.Child
as={Fragment}
enter="ease-in-out duration-500"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in-out duration-500"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="absolute top-0 left-0 -ml-8 flex pt-4 pr-2 sm:-ml-10 sm:pr-4">
<button
type="button"
className="rounded-md text-gray-300 hover:text-white focus:outline-none focus:ring-2 focus:ring-white"
onClick={() => setOpen(false)}
>
<span className="sr-only">Close panel</span>
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
</button>
</div>
</Transition.Child>
<div className="flex h-full flex-col overflow-y-scroll bg-white py-6 shadow-xl">
<div className="px-4 sm:px-6">
<Dialog.Title className="text-lg font-medium text-gray-900">{popupInfo && popupInfo.country_russian_name}</Dialog.Title>
</div>
<div className="relative mt-6 flex-1 px-4 sm:px-6">
{/* Replace with your content */}
{popupInfo && generatePopup(popupInfo)}
{/* /End replace */}
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</div>
</Dialog>
</Transition.Root>
</div>
</div>
);
}