Compare commits
No commits in common. "a5bfed73a1a67c28f5317c713aec4cf85ff78468" and "0e4d5a8d475ccc4d41a7ee7aa5f51911560bbf96" have entirely different histories.
a5bfed73a1
...
0e4d5a8d47
4 changed files with 222 additions and 85 deletions
15
package-lock.json
generated
15
package-lock.json
generated
|
|
@ -14,7 +14,6 @@
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"leaflet-heatmap": "^1.0.0",
|
"leaflet-heatmap": "^1.0.0",
|
||||||
"leaflet-timedimension": "^1.1.1",
|
|
||||||
"leaflet-velocity": "^2.1.4",
|
"leaflet-velocity": "^2.1.4",
|
||||||
"leaflet.heat": "^0.2.0"
|
"leaflet.heat": "^0.2.0"
|
||||||
},
|
},
|
||||||
|
|
@ -1103,11 +1102,6 @@
|
||||||
"@types/estree": "^1.0.6"
|
"@types/estree": "^1.0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/iso8601-js-period": {
|
|
||||||
"version": "0.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/iso8601-js-period/-/iso8601-js-period-0.2.1.tgz",
|
|
||||||
"integrity": "sha512-iDyz2TQFBd5WhCZjruOwHj01JkQGu7YbVLCVdpA7lCGEcBzE3ffCPAhLh/M8TAp//kCixPpYN4XU54WHCxvD2Q=="
|
|
||||||
},
|
|
||||||
"node_modules/js-cookie": {
|
"node_modules/js-cookie": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
|
||||||
|
|
@ -1139,15 +1133,6 @@
|
||||||
"leaflet": "*"
|
"leaflet": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/leaflet-timedimension": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/leaflet-timedimension/-/leaflet-timedimension-1.1.1.tgz",
|
|
||||||
"integrity": "sha512-ejXldN94veRsWka1vpC+4rbH+2+3d3ztn2xYx4jcXtjYDrKC/sNnoqCmyH2UEYIy51PI2851aI2k8uGdOEbhlw==",
|
|
||||||
"dependencies": {
|
|
||||||
"iso8601-js-period": "^0.2.1",
|
|
||||||
"leaflet": "~0.7.4 || ~1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/leaflet-velocity": {
|
"node_modules/leaflet-velocity": {
|
||||||
"version": "2.1.4",
|
"version": "2.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/leaflet-velocity/-/leaflet-velocity-2.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/leaflet-velocity/-/leaflet-velocity-2.1.4.tgz",
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,6 @@
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"leaflet-heatmap": "^1.0.0",
|
"leaflet-heatmap": "^1.0.0",
|
||||||
"leaflet-timedimension": "^1.1.1",
|
|
||||||
"leaflet-velocity": "^2.1.4",
|
"leaflet-velocity": "^2.1.4",
|
||||||
"leaflet.heat": "^0.2.0"
|
"leaflet.heat": "^0.2.0"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,10 @@
|
||||||
import 'leaflet-velocity/dist/leaflet-velocity.css';
|
import 'leaflet-velocity/dist/leaflet-velocity.css';
|
||||||
import 'leaflet-velocity/dist/leaflet-velocity';
|
import 'leaflet-velocity/dist/leaflet-velocity';
|
||||||
import 'leaflet.heat';
|
import 'leaflet.heat';
|
||||||
import 'leaflet-timedimension';
|
|
||||||
|
|
||||||
export let map; // принимаем карту из родительского компонента
|
export let map; // принимаем карту из родительского компонента
|
||||||
export let windData;
|
export let windData;
|
||||||
|
|
||||||
let timeDimension;
|
|
||||||
let timeDimensionControl;
|
|
||||||
let velocityLayer;
|
let velocityLayer;
|
||||||
let heatLayer;
|
let heatLayer;
|
||||||
let legend;
|
let legend;
|
||||||
|
|
@ -21,22 +18,6 @@
|
||||||
let showVectors = true;
|
let showVectors = true;
|
||||||
let layerControl;
|
let layerControl;
|
||||||
|
|
||||||
// Преобразование testVelo.json в формат timeData
|
|
||||||
const prepareTimeData = (windData) => {
|
|
||||||
if (!windData || windData.length < 2) return {};
|
|
||||||
|
|
||||||
// Используем дату из header или текущую дату, если не указана
|
|
||||||
const refTime = windData[0]?.header?.refTime || new Date().toISOString();
|
|
||||||
|
|
||||||
return {
|
|
||||||
[refTime]: {
|
|
||||||
u: windData[0].data, // U-компонента (первый объект в массиве)
|
|
||||||
v: windData[1].data // V-компонента (второй объект)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// Функция для нормализации данных тепловой карты
|
// Функция для нормализации данных тепловой карты
|
||||||
const prepareHeatData = (windData) => {
|
const prepareHeatData = (windData) => {
|
||||||
if (!windData || windData.length < 2) {
|
if (!windData || windData.length < 2) {
|
||||||
|
|
@ -74,9 +55,9 @@
|
||||||
// Вычисляем координаты для текущей точки
|
// Вычисляем координаты для текущей точки
|
||||||
const y = Math.floor(i / nx);
|
const y = Math.floor(i / nx);
|
||||||
const x = i % nx;
|
const x = i % nx;
|
||||||
let lat = la1 - y * dy;
|
const lat = la1 - y * dy;
|
||||||
let lng = lo1 + x * dx;
|
const lng = lo1 + x * dx;
|
||||||
if (lng >= 180) lng -= 360;
|
|
||||||
heatData.push([lat, lng, speed]);
|
heatData.push([lat, lng, speed]);
|
||||||
maxSpeed = Math.max(maxSpeed, speed);
|
maxSpeed = Math.max(maxSpeed, speed);
|
||||||
}
|
}
|
||||||
|
|
@ -101,9 +82,9 @@
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return L.heatLayer(data, {
|
return L.heatLayer(data, {
|
||||||
radius: 8, // Увеличьте радиус для глобальной карты
|
radius: 20, // Увеличьте радиус для глобальной карты
|
||||||
blur: 20,
|
blur: 15,
|
||||||
// maxZoom: 10,
|
maxZoom: 10,
|
||||||
minOpacity: 0.7,
|
minOpacity: 0.7,
|
||||||
gradient: {
|
gradient: {
|
||||||
0.1: 'blue',
|
0.1: 'blue',
|
||||||
|
|
@ -208,50 +189,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!map) return;
|
updateLayers();
|
||||||
|
|
||||||
// 1. Настройка TimeDimension (добавьте эти строки в начале)
|
|
||||||
// L.TimeDimension.Util.setProxy('https://your-proxy.com/?url='); // Для загрузки больших данных
|
|
||||||
L.TimeDimension.Util.setCacheLimit(10); // Лимит кэшированных кадров
|
|
||||||
|
|
||||||
// 1. Подготовка данных
|
|
||||||
const timeData = prepareTimeData(windData);
|
|
||||||
const firstTime = Object.keys(timeData)[0];
|
|
||||||
|
|
||||||
// Инициализация TimeDimension
|
|
||||||
timeDimension = new L.TimeDimension({
|
|
||||||
period: "PT1H", // Интервал 1 час
|
|
||||||
timeInterval: '${firstTime}/${firstTime}',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Добавляем контролы времени
|
|
||||||
timeDimensionControl = new L.Control.TimeDimension({
|
|
||||||
timeDimension,
|
|
||||||
position: 'bottomleft',
|
|
||||||
// autoPlay: true,
|
|
||||||
playerOptions: {
|
|
||||||
// transitionTime: 1000,
|
|
||||||
loop: false,
|
|
||||||
minBufferReady: -1
|
|
||||||
}
|
|
||||||
});
|
|
||||||
map.addControl(timeDimensionControl);
|
|
||||||
|
|
||||||
// 4. Создание слоев
|
|
||||||
const velocityLayer = L.timeDimension.layer.windVelocity({
|
|
||||||
displayValues: true,
|
|
||||||
data: timeData,
|
|
||||||
displayOptions: {
|
|
||||||
velocityType: 'Wind Speed',
|
|
||||||
position: 'bottomleft'
|
|
||||||
}
|
|
||||||
}).addTo(map);
|
|
||||||
|
|
||||||
// 5. Тепловая карта (адаптируйте под ваш формат)
|
|
||||||
const heatLayer = L.timeDimension.layer.heat({
|
|
||||||
radius: 15,
|
|
||||||
data: prepareTimeHeatData(timeData)
|
|
||||||
}).addTo(map);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,215 @@
|
||||||
|
<!-- Timeline.svelte -->
|
||||||
|
<script>
|
||||||
|
import { onMount, onDestroy } from 'svelte';
|
||||||
|
|
||||||
|
export let initialHour = 0;
|
||||||
|
export let totalHours = 72; // 3 суток
|
||||||
|
export let updateInterval = 3; // шаг в 3 часа
|
||||||
|
export let playSpeed = 2000; // скорость анимации (мс)
|
||||||
|
|
||||||
|
let currentHour = initialHour;
|
||||||
|
let isPlaying = false;
|
||||||
|
let timer;
|
||||||
|
|
||||||
|
// События для коммуникации с родительским компонентом
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
// Автоматическое проигрывание
|
||||||
|
function togglePlay() {
|
||||||
|
isPlaying = !isPlaying;
|
||||||
|
|
||||||
|
if (isPlaying) {
|
||||||
|
timer = setInterval(() => {
|
||||||
|
currentHour = (currentHour + updateInterval) % totalHours;
|
||||||
|
dispatchTimeUpdate();
|
||||||
|
}, playSpeed);
|
||||||
|
} else {
|
||||||
|
clearInterval(timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dispatchTimeUpdate() {
|
||||||
|
dispatch('timeupdate', {
|
||||||
|
hour: currentHour,
|
||||||
|
timestamp: calculateTimestamp(currentHour)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateTimestamp(hour) {
|
||||||
|
const now = new Date();
|
||||||
|
now.setHours(now.getHours() + hour);
|
||||||
|
return now;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(hours) {
|
||||||
|
const days = Math.floor(hours / 24);
|
||||||
|
const remainingHours = hours % 24;
|
||||||
|
const time = calculateTimestamp(hours);
|
||||||
|
|
||||||
|
return `+${days}d ${remainingHours}h (${time.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Очистка при размонтировании
|
||||||
|
onDestroy(() => {
|
||||||
|
clearInterval(timer);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="timeline-container">
|
||||||
|
<div class="timeline-controls">
|
||||||
|
<button
|
||||||
|
on:click={() => {
|
||||||
|
currentHour = Math.max(0, currentHour - updateInterval);
|
||||||
|
dispatchTimeUpdate();
|
||||||
|
}}
|
||||||
|
disabled={currentHour <= 0}
|
||||||
|
class="control-button"
|
||||||
|
>
|
||||||
|
← Назад
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
on:click={togglePlay}
|
||||||
|
class="control-button play-button {isPlaying ? 'playing' : ''}"
|
||||||
|
>
|
||||||
|
{isPlaying ? '❚❚' : '▶'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
on:click={() => {
|
||||||
|
currentHour = Math.min(totalHours, currentHour + updateInterval);
|
||||||
|
dispatchTimeUpdate();
|
||||||
|
}}
|
||||||
|
disabled={currentHour >= totalHours}
|
||||||
|
class="control-button"
|
||||||
|
>
|
||||||
|
Вперёд →
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="time-display">
|
||||||
|
{formatTime(currentHour)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max={totalHours}
|
||||||
|
step={updateInterval}
|
||||||
|
bind:value={currentHour}
|
||||||
|
on:input={() => dispatchTimeUpdate()}
|
||||||
|
class="timeline-slider"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="time-marks">
|
||||||
|
{#each Array(Math.floor(totalHours / updateInterval) + 1) as _, i}
|
||||||
|
<div
|
||||||
|
class="time-mark {currentHour === i * updateInterval ? 'active' : ''}"
|
||||||
|
on:click={() => {
|
||||||
|
currentHour = i * updateInterval;
|
||||||
|
dispatchTimeUpdate();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{i * updateInterval % 24 === 0 ? `День ${Math.floor(i * updateInterval / 24) + 1}` : ''}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.timeline-container {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 90%;
|
||||||
|
max-width: 800px;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button {
|
||||||
|
background: #f0f0f0;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button:hover {
|
||||||
|
background: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-button {
|
||||||
|
width: 40px;
|
||||||
|
background: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-button.playing {
|
||||||
|
background: #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-display {
|
||||||
|
font-weight: bold;
|
||||||
|
min-width: 180px;
|
||||||
|
text-align: center;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-slider {
|
||||||
|
width: 100%;
|
||||||
|
margin: 5px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-marks {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-mark {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-mark:before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -15px;
|
||||||
|
left: 50%;
|
||||||
|
width: 1px;
|
||||||
|
height: 10px;
|
||||||
|
background: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-mark.active {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2196F3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-mark.active:before {
|
||||||
|
background: #2196F3;
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue