1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
|
// Cloudflare Workers scheduled handler for cron jobs
import type { ScheduledController, ScheduledEvent } from '@cloudflare/workers-types';
import { DateTime } from 'luxon';
interface Env {
IMGS_BUCKET: R2Bucket;
}
interface RadarImage {
id: string;
forecast_time: string;
modified: string;
created: string;
file_name: string;
type: string;
}
interface RadarResponse {
data: {
types: {
IMSRadar: RadarImage[];
};
};
}
class GeshemUpdate {
private bucket: R2Bucket;
private static readonly API_URL = 'https://ims.gov.il/he/radar_satellite';
private static readonly BASE_URL = 'https://ims.gov.il';
constructor(bucket: R2Bucket) {
this.bucket = bucket;
}
private convertIsraeliToUTC(dateStr: string, timeStr: string): { date: string; time: string } {
// Parse YYYYMMDD HHMM format in Israeli timezone
const israeliTime = DateTime.fromFormat(
`${dateStr} ${timeStr}`,
'yyyyMMdd HHmm',
{ zone: 'Asia/Jerusalem' }
);
// Convert to UTC
const utcTime = israeliTime.toUTC();
// Format as YYYYMMDD and HHMM
return {
date: utcTime.toFormat('yyyyMMdd'),
time: utcTime.toFormat('HHmm')
};
}
private async getAllImages(): Promise<RadarImage[]> {
const response = await fetch(GeshemUpdate.API_URL);
if (!response.ok) {
throw new Error(`Failed to fetch radar data: ${response.statusText}`);
}
const data: RadarResponse = await response.json();
const images = data.data.types.IMSRadar;
if (!images || images.length === 0) {
return [];
}
return images;
}
private parseFilename(filepath: string): { date: string; time: string } | null {
// Extract filename from path: /sites/default/files/ims_data/map_images/IMSRadar4GIS/IMSRadar4GIS_202510030935_0.png
const filename = filepath.split('/').pop();
if (!filename) return null;
// Match pattern: IMSRadar4GIS_YYYYMMDDHHMM_0.png
const match = filename.match(/IMSRadar4GIS_(\d{8})(\d{4})_\d\.png/);
if (!match) return null;
const dateStr = match[1]; // YYYYMMDD
const timeStr = match[2]; // HHMM
return { date: dateStr, time: timeStr };
}
private async getExistingGisKeys(): Promise<Set<string>> {
// Get yesterday's date in YYYYMMDD format
const yesterday = DateTime.utc().minus({ days: 1 });
const yesterdayStr = yesterday.toFormat('yyyyMMdd');
console.log(`Listing bucket keys starting from ${yesterdayStr}`);
const existingKeys = new Set<string>();
try {
const objects = await this.bucket.list({
prefix: 'imgs/',
startAfter: `imgs/${yesterdayStr}`
});
for (const obj of objects.objects) {
// Only include keys that end with gis.png
if (obj.key.endsWith('gis.png')) {
existingKeys.add(obj.key);
}
}
console.log(`Found ${existingKeys.size} existing gis.png files since ${yesterdayStr}`);
} catch (error) {
console.error('Error listing bucket objects:', error);
}
return existingKeys;
}
private async fetchAndStoreImages(): Promise<number> {
// Get all images from API
const apiImages = await this.getAllImages();
if (apiImages.length === 0) {
console.log('No images found in API response');
return 0;
}
console.log(`Found ${apiImages.length} images in API response`);
// Get existing gis.png keys in bucket
const existingKeys = await this.getExistingGisKeys();
let savedCount = 0;
for (const apiImage of apiImages) {
console.log(`Processing image: ${apiImage.file_name}`);
// Parse filename to get date and time
const parsed = this.parseFilename(apiImage.file_name);
if (!parsed) {
console.error(`Failed to parse filename: ${apiImage.file_name}`);
continue;
}
console.log(`Parsed Israeli time - Date: ${parsed.date}, Time: ${parsed.time}`);
// Convert Israeli time to UTC
const utc = this.convertIsraeliToUTC(parsed.date, parsed.time);
console.log(`Converted to UTC - Date: ${utc.date}, Time: ${utc.time}`);
// Build R2 key: imgs/YYYYMMDD/HHMM/gis.png
const key = `imgs/${utc.date}/${utc.time}/gis.png`;
// Check if already exists
if (existingKeys.has(key)) {
console.log(`Image already exists at ${key}, skipping`);
continue;
}
// Fetch the image
const imageUrl = `${GeshemUpdate.BASE_URL}${apiImage.file_name}`;
console.log(`Downloading from ${imageUrl}`);
const response = await fetch(imageUrl);
if (!response.ok) {
console.error(`Failed to fetch image: ${response.statusText}`);
continue;
}
const imageData = await response.arrayBuffer();
// Upload to R2
console.log(`Uploading to ${key}`);
await this.bucket.put(key, imageData, {
httpMetadata: {
contentType: 'image/png',
cacheControl: 'public, max-age=31536000'
}
});
savedCount++;
}
return savedCount;
}
async run(): Promise<string> {
try {
const savedCount = await this.fetchAndStoreImages();
return `Saved ${savedCount} new images`;
} catch (error) {
console.error('Error in GeshemUpdate.run():', error);
throw error;
}
}
}
export async function handleSchedule(
controller: ScheduledController,
env: Env,
ctx: ExecutionContext
): Promise<void> {
console.log('Cron job triggered at', new Date().toISOString());
try {
if (!env.IMGS_BUCKET) {
throw new Error('R2 bucket not configured');
}
const updater = new GeshemUpdate(env.IMGS_BUCKET);
const result = await updater.run();
console.log(`Cron job completed: ${result}`);
} catch (error) {
console.error('Error in scheduled function:', error);
throw error;
}
}
export default {
async scheduled(
controller: ScheduledController,
env: Env,
ctx: ExecutionContext
): Promise<void> {
return handleSchedule(controller, env, ctx);
}
};
|