Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
ae5bb1b
feat: added init timeslider code
srijitcoder Sep 2, 2025
07090f4
feat: added some more changes to timescale based on figma
srijitcoder Sep 3, 2025
60a4109
feat: made values dynamic and attached with map layer
srijitcoder Sep 4, 2025
5c28973
feat: added items filter and made date select dynamic
srijitcoder Sep 4, 2025
eb25c95
fix: itemsfilter reset issue
srijitcoder Sep 4, 2025
d927157
feat: added dispatch event for selected items
srijitcoder Sep 4, 2025
344ca7f
fix:wrong time output going into eox-map
srijitcoder Sep 4, 2025
d89764e
feat: cleaned code
srijitcoder Sep 9, 2025
dcf63f2
feat: improved config logic
srijitcoder Sep 9, 2025
4755a3f
feat: add dynamic layer change timeslider update
srijitcoder Sep 9, 2025
244fc7a
feat: buch of changes and features added
srijitcoder Sep 22, 2025
19ea08c
feat: added utc based date query, added calendar experiment and multi…
srijitcoder Sep 25, 2025
c27ce91
feat: added custom calendar using vanila calendar pro
srijitcoder Sep 26, 2025
2b501a0
feat: added calendar dots renderer
srijitcoder Sep 26, 2025
25ae762
feat: added settings and updated external example and reverted items …
srijitcoder Sep 30, 2025
2af68d9
chore: conflict resolved
srijitcoder Sep 30, 2025
f8713a2
feat: added range selction dynamic
srijitcoder Oct 6, 2025
7dd7c38
feat: added first set of export aniamtion feature
srijitcoder Oct 8, 2025
ec2acee
feat: added gif generation
srijitcoder Oct 8, 2025
9873028
feat: added animation for non external rendering
srijitcoder Oct 9, 2025
c1b262a
feat: some example zoom in changes, styling changes and timeslider dr…
srijitcoder Oct 14, 2025
b96b61e
feat: update playback UI based on new figma design
srijitcoder Oct 15, 2025
b37caf2
feat: updated animate ui and added mp4 export
srijitcoder Oct 17, 2025
ec64022
chore: removed old setting
srijitcoder Oct 17, 2025
ba43ab6
chroe: add example with multiple images
silvester-pari Oct 27, 2025
1cc49d8
chore: rewrite example to account for map movement
silvester-pari Oct 27, 2025
a4c5f84
chore: adjust example
silvester-pari Oct 27, 2025
803de38
Merge branch 'main' into feat/timeslider/init
silvester-pari Oct 27, 2025
ed52acc
chore(deps): update package lock
silvester-pari Oct 27, 2025
3adcdcc
fix(deps): package lock
silvester-pari Oct 27, 2025
d299978
feat: added sentinal 2 example with animation
srijitcoder Oct 27, 2025
fbb6db1
chore: add mosic example
silvester-pari Nov 4, 2025
6f5bbb8
chore: use latest data
silvester-pari Nov 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .storybook/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import "@eox/map/src/plugins/advancedLayersAndSources";
import "@eox/stacinfo";
import "@eox/storytelling";
import "@eox/timecontrol";
import "@eox/timeslider";

/**
* A custom wrapper for the default setCustomElementsManifest function.
Expand Down
3 changes: 3 additions & 0 deletions elements/map/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,9 @@ export class EOxMap extends LitElement {
*/
set layers(layers) {
this.#layers = setLayersMethod(layers, this.#layers, this);
this.dispatchEvent(
new CustomEvent("layerschanged", { detail: { layers: this.#layers } }),
);
}

/**
Expand Down
Empty file added elements/timeslider/README.md
Empty file.
57 changes: 57 additions & 0 deletions elements/timeslider/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"name": "@eox/timeslider",
"version": "0.0.1",
"type": "module",
"repository": {
"type": "git",
"url": "[email protected]:EOX-A/EOxElements.git"
},
"devDependencies": {
"ol": "^10.4.0",
"vite": "^6.0.3"
},
"engines": {
"npm": ">=8.0.0",
"node": ">=18.0.0"
},
"files": [
"dist",
"src"
],
"main": "./src/main.js",
"types": "./types/timeslider/src/main.d.ts",
"typesVersions": {
"*": {
".": [
"./types/timeslider/src/main.d.ts"
],
"src/*": [
"./types/timeslider/src/*"
]
}
},
"scripts": {
"types:generate": "tsc --project tsconfig.build.json || true",
"prepack": "npm run types:generate",
"build": "vite build",
"watch": "vite build --watch"
},
"dependencies": {
"@eox/elements-utils": "^1.1.0",
"@eox/ui": "^0.3.6",
"@ffmpeg/ffmpeg": "^0.12.15",
"@ffmpeg/util": "^0.12.2",
"@ffmpeg/core": "^0.12.6",
"dayjs": "^1.11.9",
"gifshot": "^0.4.5",
"lit": "^3.2.0",
"lodash.groupby": "^4.6.0",
"lodash.isequal": "^4.5.0",
"uuid": "^13.0.0",
"vanilla-calendar-pro": "^3.0.5",
"vis-timeline": "^8.3.0"
},
"peerDependencies": {
"@eox/map": "*"
}
}
2 changes: 2 additions & 0 deletions elements/timeslider/src/enums/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { DEFAULT_ARGS } from "./stories";
export { DEFAULT_TIMELINE_OPTIONS, DATE_FORMATS } from "./timeline-config";
54 changes: 54 additions & 0 deletions elements/timeslider/src/enums/stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
export const DEFAULT_ARGS = {
layer: "AWS_NO2-VISUALISATION",
controlProperty: "TIME",
controlValues: [
"2022-12-05",
"2022-12-12",
"2022-12-19",
"2022-12-26",
"2023-01-16",
"2023-01-23",
"2023-01-30",
"2023-02-06",
"2023-02-13",
"2023-02-27",
"2023-03-06",
"2023-03-13",
"2023-03-20",
"2023-03-27",
"2023-04-03",
"2023-04-10",
"2023-04-17",
"2023-04-24",
],
navigation: true,
play: true,
// map
layers: [
{
type: "Tile",
properties: {
id: "AWS_NO2-VISUALISATION",
},
source: {
type: "TileWMS",
url: "https://services.sentinel-hub.com/ogc/wms/0635c213-17a1-48ee-aef7-9d1731695a54",
params: {
LAYERS: "AWS_NO2-VISUALISATION",
TIME: "2022-12-05",
},
},
},
{
type: "Tile",
properties: {
id: "OSM",
},
source: {
type: "OSM",
},
},
],
center: [1000000, 6000000],
zoom: 3,
};
44 changes: 44 additions & 0 deletions elements/timeslider/src/enums/timeline-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* Default timeline configuration options
*/
export const DEFAULT_TIMELINE_OPTIONS = {
stack: false,
selectable: true,
zoomable: true,
moveable: true,
margin: { item: 40, axis: 20 },
showCurrentTime: true,
timeAxis: {
scale: "day",
step: 5,
},
orientation: { axis: "top" },
};

/**
* Date format configurations for vis-timeline
*/
export const DATE_FORMATS = {
minorLabels: {
millisecond: "SSS",
second: "s",
minute: "HH:mm",
hour: "HH:mm",
weekday: "ddd D",
day: "DD",
week: "w",
month: "MMM",
year: "YYYY",
},
majorLabels: {
millisecond: "HH:mm:ss",
second: "D MMMM HH:mm",
minute: "ddd D MMMM",
hour: "ddd D MMMM",
weekday: "MMM YYYY",
day: "MMM YYYY",
week: "MMM YYYY",
month: "YYYY",
year: "",
},
};
21 changes: 21 additions & 0 deletions elements/timeslider/src/helpers/create-timeline-options.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {
DEFAULT_TIMELINE_OPTIONS,
DATE_FORMATS,
} from "../enums/timeline-config";

/**
* Creates vis-timeline options configuration
* @param {string} min - Minimum date string
* @param {string} max - Maximum date string
* @returns {Object} Timeline options configuration
*/
export default function createTimelineOptions(min, max) {
return {
...DEFAULT_TIMELINE_OPTIONS,
start: min,
end: max,
min: min,
max: max,
format: DATE_FORMATS,
};
}
196 changes: 196 additions & 0 deletions elements/timeslider/src/helpers/export-animation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import { FFmpeg } from "@ffmpeg/ffmpeg";
import { toBlobURL } from "@ffmpeg/util";
import { createGIF } from "gifshot";

export default async function exportAnimation(mapLayers, type, fps, that) {
const images = mapLayers.map((layer) => layer.img).filter((img) => img);

if (images.length === 0) {
console.error("No images available for export");
return;
}

if (type === "mp4") {
try {
const blob = await imagesToMp4(images, { fps, preset: "ultrafast" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "timeslider.mp4";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
} catch (error) {
console.error("MP4 conversion error:", error);
alert(
"Failed to convert to MP4: " +
(error instanceof Error ? error.message : String(error)),
);
} finally {
}
} else {
imagesToGIF(images, that, fps);
}
}

function imagesToGIF(dataUrls, that, fps) {
const map = that.renderRoot.querySelector(".map-view-item");
const mapBounding = map.getBoundingClientRect();
const mapWidth = mapBounding.width;
const mapHeight = mapBounding.height;

createGIF(
{
gifWidth: mapWidth,
gifHeight: mapHeight,
images: [...dataUrls],
interval: 1,
numFrames: dataUrls.length,
frameDuration: fps,
fontWeight: "normal",
fontSize: "16px",
fontFamily: "sans-serif",
fontColor: "#ffffff",
textAlign: "center",
textBaseline: "bottom",
sampleInterval: 10,
numWorkers: 2,
},
function (obj) {
if (!obj.error) {
// Download the generated GIF image
const link = document.createElement("a");
link.href = obj.image;
link.download = "timeslider.gif";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} else {
console.error("GIF generation error:", obj.error);
}
},
);
}

async function imagesToMp4(
dataUrls,
{ fps = 1, crf = 1, preset = "veryfast" } = {},
) {
if (!dataUrls || dataUrls.length === 0) throw new Error("No frames provided");

// Use FFmpeg's new API for version 0.12+
const baseURL = "https://unpkg.com/@ffmpeg/[email protected]/dist/esm";
const ffmpeg = new FFmpeg();

// Load with proper worker and wasm URLs
await ffmpeg.load({
coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, "text/javascript"),
wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, "application/wasm"),
});

// Write each image as a uniquely named PNG
const pad = Math.max(3, String(dataUrls.length - 1).length + 1); // always at least 3 digits
const nameFor = (i) => `frame${String(i).padStart(pad, "0")}.png`;

for (let i = 0; i < dataUrls.length; i++) {
const url = dataUrls[i];

// Convert dataURL to ArrayBuffer
let buf;
try {
if (url.startsWith("data:")) {
// Handle dataURL directly (faster)
const base64Data = url.split(",")[1];
if (!base64Data) {
throw new Error(`Invalid data URL format at index ${i}`);
}
const byteString = atob(base64Data);
const array = new Uint8Array(byteString.length);
for (let j = 0; j < byteString.length; j++) {
array[j] = byteString.charCodeAt(j);
}
buf = array;
} else {
// Remote fetch as before
const resp = await fetch(url);
if (!resp.ok) {
throw new Error(
`Failed to fetch image ${i}: ${resp.status} ${resp.statusText}`,
);
}
buf = new Uint8Array(await resp.arrayBuffer());
}

if (buf.length === 0) {
throw new Error(`Empty image data at index ${i}`);
}

await ffmpeg.writeFile(nameFor(i), buf);
} catch (error) {
throw new Error(
`Failed to process image ${i}: ${error instanceof Error ? error.message : String(error)}`,
);
}
}

// Assemble the input pattern ('frame%03d.png' etc)
const inputPattern = `frame%0${pad}d.png`;
const outputFile = "output.mp4";

try {
await ffmpeg.exec([
"-framerate",
String(fps),
"-i",
inputPattern,
"-movflags",
"faststart",
"-pix_fmt",
"yuv420p",
"-vf",
"scale=trunc(iw/2)*2:trunc(ih/2)*2",
"-c:v",
"libx264",
"-preset",
preset,
"-crf",
String(crf),
"-r",
String(fps),
outputFile,
]);

const data = await ffmpeg.readFile(outputFile);

if (!data || data.length === 0) {
throw new Error("FFmpeg produced no output");
}

// Clean up temporary files
try {
for (let i = 0; i < dataUrls.length; i++) {
await ffmpeg.deleteFile(nameFor(i));
}
await ffmpeg.deleteFile(outputFile);
} catch (cleanupError) {
console.warn("Failed to clean up temporary files:", cleanupError);
}

// data is a Uint8Array, convert to ArrayBuffer for Blob
return new Blob([data.buffer], { type: "video/mp4" });
} catch (error) {
// Clean up on error
try {
for (let i = 0; i < dataUrls.length; i++) {
await ffmpeg.deleteFile(nameFor(i));
}
await ffmpeg.deleteFile(outputFile);
} catch (cleanupError) {
// Ignore cleanup errors
}
throw new Error(
`FFmpeg encoding failed: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
Loading