File Coverage

TimeZoneFinder.xs
Criterion Covered Total %
statement 220 248 88.7
branch 122 166 73.4
condition n/a
subroutine n/a
pod n/a
total 342 414 82.6


line stmt bran cond sub pod time code
1             /*
2             * Map geographic coordinates to time zone names
3             *
4             * Copyright (C) 2023 Andreas Vögele
5             *
6             * This module is free software; you can redistribute it and/or modify it
7             * under the same terms as Perl itself.
8             */
9              
10             /* SPDX-License-Identifier: Artistic-1.0-Perl OR GPL-1.0-or-later */
11              
12             #define PERL_NO_GET_CONTEXT
13             #include "EXTERN.h"
14             #include "perl.h"
15             #define NO_XSLOCKS
16             #include "XSUB.h"
17              
18             #ifdef MULTIPLICITY
19             #define storeTHX(var) (var) = aTHX
20             #define dTHXfield(var) tTHX var;
21             #else
22             #define storeTHX(var) dNOOP
23             #define dTHXfield(var)
24             #endif
25              
26             #include "shapereader/shapereader.h"
27             #include
28             #include
29             #include
30              
31             /* Index the shapes by their bounding box. */
32             struct index_entry {
33             shp_box_t box; /* Bounding box from the shp file */
34             size_t file_offset; /* File position of the corresponding polygon */
35             SV *time_zone; /* Time zone name from the dbf file */
36             };
37              
38             struct index {
39             size_t num_entries;
40             struct index_entry *entries;
41             struct index_entry **matches; /* Entries that match a location */
42             };
43              
44             typedef struct geo_location_timezonefinder {
45             SV *dbf_filename;
46             SV *shp_filename;
47             FILE *dbf_fp;
48             FILE *shp_fp;
49             size_t dbf_num;
50             size_t shp_num;
51             struct index index;
52             dbf_file_t dbf_fh;
53             shp_file_t shp_fh;
54             dTHXfield(perl)
55             } *Geo__Location__TimeZoneFinder;
56              
57             static void
58 1           init_index(Geo__Location__TimeZoneFinder self, size_t num_entries)
59             {
60             dTHXa(self->perl);
61             struct index *index;
62              
63 1           index = &self->index;
64 1 50         Newxz(index->entries, num_entries, struct index_entry);
65 1 50         Newxz(index->matches, num_entries, struct index_entry *);
66 1           index->num_entries = num_entries;
67 1           }
68              
69             static void
70 2           free_index(Geo__Location__TimeZoneFinder self)
71             {
72             dTHXa(self->perl);
73             struct index *index;
74             struct index_entry *entry;
75             size_t n, i;
76              
77 2           index = &self->index;
78 2 100         if (index->entries != NULL) {
79 1           n = index->num_entries;
80 7 100         for (i = 0; i < n; ++i) {
81 6           entry = &index->entries[i];
82 6           SvREFCNT_dec(entry->time_zone);
83             }
84 1           Safefree(index->entries);
85 1           index->entries = NULL;
86             }
87 2 100         if (index->matches != NULL) {
88 1           Safefree(index->matches);
89 1           index->matches = NULL;
90             }
91 2           }
92              
93             static void
94 2           free_self(Geo__Location__TimeZoneFinder self)
95             {
96             dTHXa(self->perl);
97              
98 2           free_index(self);
99 2 50         if (self->dbf_fp != NULL) {
100 0           (void) fclose(self->dbf_fp);
101 0           self->dbf_fp = NULL;
102             }
103 2 100         if (self->shp_fp != NULL) {
104 1           (void) fclose(self->shp_fp);
105 1           self->shp_fp = NULL;
106             }
107 2 50         if (self->dbf_filename != NULL) {
108 2           SvREFCNT_dec(self->dbf_filename);
109             }
110 2 50         if (self->shp_filename != NULL) {
111 2           SvREFCNT_dec(self->shp_filename);
112             }
113 2           Safefree(self);
114 2           }
115              
116             static int
117 7           is_tzid(const dbf_field_t *field)
118             {
119 7           return field->type == DBFT_CHARACTER;
120             }
121              
122             static int
123 1           handle_dbf_header(dbf_file_t *fh, const dbf_header_t *header)
124             {
125 1           Geo__Location__TimeZoneFinder self =
126             (Geo__Location__TimeZoneFinder) fh->user_data;
127             const dbf_field_t *field;
128             int has_tzid;
129              
130 1           self->dbf_num = 0;
131              
132 1           has_tzid = 0;
133              
134 1           field = header->fields;
135 1 50         while (field != NULL) {
136 1 50         if (is_tzid(field)) {
137 1           has_tzid = 1;
138 1           break;
139             }
140 0           field = field->next;
141             }
142              
143 1 50         if (!has_tzid) {
144 0           dbf_error(fh, "No tzid field");
145 0           return -1;
146             }
147              
148 1 50         if (header->num_records == 0) {
149 0           dbf_error(fh, "No records");
150 0           return -1;
151             }
152              
153 1           init_index(self, header->num_records);
154              
155 1           return 1;
156             }
157              
158             static int
159 6           handle_dbf_record(dbf_file_t *fh, const dbf_header_t *header,
160             const dbf_record_t *record, size_t file_offset)
161             {
162 6           Geo__Location__TimeZoneFinder self =
163             (Geo__Location__TimeZoneFinder) fh->user_data;
164             dTHXa(self->perl);
165             struct index *index;
166             struct index_entry *entry;
167             const dbf_field_t *field;
168             const char *s;
169             size_t len;
170              
171 6           index = &self->index;
172              
173 6 50         if (dbf_record_is_deleted(record)) {
174 0           return 1;
175             }
176              
177 6 50         if (self->dbf_num >= index->num_entries) {
178 0           dbf_error(fh, "Expected %zu records, got %zu", index->num_entries,
179             self->dbf_num);
180 0           return -1;
181             }
182              
183 6           entry = &index->entries[self->dbf_num];
184 6           field = header->fields;
185 6 50         while (field != NULL) {
186 6 50         if (is_tzid(field)) {
187             /* Time zone names are plain ASCII. */
188 6           dbf_record_string(record, field, &s, &len);
189 6           entry->time_zone = newSVpv(s, len);
190 6           ++self->dbf_num;
191 6           break;
192             }
193 0           field = field->next;
194             }
195              
196 6           return 1;
197             }
198              
199             static int
200 1           handle_shp_header(shp_file_t *fh, const shp_header_t *header)
201             {
202 1           Geo__Location__TimeZoneFinder self =
203             (Geo__Location__TimeZoneFinder) fh->user_data;
204              
205 1           self->shp_num = 0;
206              
207 1           return 1;
208             }
209              
210             static int
211 6           handle_shp_record(shp_file_t *fh, const shp_header_t *header,
212             const shp_record_t *record, size_t file_offset)
213             {
214 6           Geo__Location__TimeZoneFinder self =
215             (Geo__Location__TimeZoneFinder) fh->user_data;
216             struct index *index;
217             struct index_entry *entry;
218              
219 6           index = &self->index;
220              
221 6 50         if (self->shp_num >= index->num_entries) {
222 0           shp_error(fh, "Expected %zu records, got %zu", index->num_entries,
223             self->shp_num);
224 0           return -1;
225             }
226              
227 6           entry = &index->entries[self->shp_num];
228 6 50         if (record->shape_type == SHPT_POLYGON) {
229 6           entry->box = record->shape.polygon.box;
230 6           entry->file_offset = file_offset;
231 6           ++self->shp_num;
232             }
233              
234 6           return 1;
235             }
236              
237             struct ocean_zone {
238             const char *tzid;
239             double left;
240             double right;
241             };
242              
243             static const struct ocean_zone ocean_zones[25] = {
244             {"Etc/GMT-12", 172.5, 180.0},
245             {"Etc/GMT-11", 157.5, 172.5},
246             {"Etc/GMT-10", 142.5, 157.5},
247             {"Etc/GMT-9", 127.5, 142.5},
248             {"Etc/GMT-8", 112.5, 127.5},
249             {"Etc/GMT-7", 97.5, 112.5},
250             {"Etc/GMT-6", 82.5, 97.5},
251             {"Etc/GMT-5", 67.5, 82.5},
252             {"Etc/GMT-4", 52.5, 67.5},
253             {"Etc/GMT-3", 37.5, 52.5},
254             {"Etc/GMT-2", 22.5, 37.5},
255             {"Etc/GMT-1", 7.5, 22.5},
256             {"Etc/GMT", -7.5, 7.5},
257             {"Etc/GMT+1", -22.5, -7.5},
258             {"Etc/GMT+2", -37.5, -22.5},
259             {"Etc/GMT+3", -52.5, -37.5},
260             {"Etc/GMT+4", -67.5, -52.5},
261             {"Etc/GMT+5", -82.5, -67.5},
262             {"Etc/GMT+6", -97.5, -82.5},
263             {"Etc/GMT+7", -112.5, -97.5},
264             {"Etc/GMT+8", -127.5, -112.5},
265             {"Etc/GMT+9", -142.5, -127.5},
266             {"Etc/GMT+10", -157.5, -142.5},
267             {"Etc/GMT+11", -172.5, -157.5},
268             {"Etc/GMT+12", -180.0, -172.5}
269             };
270              
271             static void
272 10           get_special_time_zones(Geo__Location__TimeZoneFinder self,
273             const shp_point_t *location, AV *time_zones)
274             {
275             dTHXa(self->perl);
276             SV *time_zone;
277             double lat, lon;
278             int i;
279              
280 10           lon = location->x;
281 10           lat = location->y;
282 10 100         if (lat == 90.0) {
283             /* North Pole */
284 26 100         for (i = 0; i < 25; ++i) {
285 25           time_zone = newSVpv(ocean_zones[i].tzid, 0);
286 25           av_push(time_zones, time_zone);
287             }
288             }
289 9 100         else if (lon == -180.0 || lon == 180.0) {
    100          
290             /* International Date Line */
291 2           time_zone = newSVpv(ocean_zones[0].tzid, 0);
292 2           av_push(time_zones, time_zone);
293 2           time_zone = newSVpv(ocean_zones[24].tzid, 0);
294 2           av_push(time_zones, time_zone);
295             }
296 10           }
297              
298             static void
299 4           get_time_zones_at_sea(Geo__Location__TimeZoneFinder self,
300             const shp_point_t *location, AV *time_zones)
301             {
302             dTHXa(self->perl);
303             SV *time_zone;
304             const struct ocean_zone *z;
305             double lon;
306             int i;
307              
308 4           lon = location->x;
309 66 100         for (i = 0; i < 25; ++i) {
310 65           z = &ocean_zones[i];
311 65 100         if (lon >= z->left && lon <= z->right) {
    100          
312 5           time_zone = newSVpv(z->tzid, 0);
313 5           av_push(time_zones, time_zone);
314             }
315 65 100         if (lon >= z->right) {
316 3           break;
317             }
318             }
319 4           }
320              
321             static void
322 7           get_time_zones(Geo__Location__TimeZoneFinder self,
323             const shp_point_t *location, AV *time_zones)
324             {
325             dTHXa(self->perl);
326             struct index *index;
327             struct index_entry *entry;
328             size_t n, m, i;
329             size_t offset;
330             FILE *fp;
331             shp_file_t *fh;
332             shp_record_t *record;
333             shp_polygon_t *polygon;
334              
335 7           index = &self->index;
336              
337             /* How many bounding boxes contain the location? */
338 7           m = 0;
339 7           n = index->num_entries;
340 49 100         for (i = 0; i < n; ++i) {
341 42           entry = &index->entries[i];
342 42 100         if (shp_box_point_in_box(&entry->box, location) != 0) {
343 6           index->matches[m] = entry;
344 6           ++m;
345             }
346             }
347              
348             /* If there is only one match, return immediately. */
349 7 100         if (m == 1) {
350 2           entry = index->matches[0];
351 2           av_push(time_zones, SvREFCNT_inc(entry->time_zone));
352 2           return;
353             }
354              
355             /* Otherwise, check the polygons in the shp file. */
356 5           fh = &self->shp_fh;
357 5           fp = fh->fp;
358 9 100         for (i = 0; i < m; ++i) {
359 4           entry = index->matches[i];
360 4           offset = entry->file_offset;
361              
362 4 50         if (offset > (size_t) LONG_MAX
363 4 50         || fseek(fp, (long) offset, SEEK_SET) != 0) {
364 0           croak("Cannot set file position to %zu in \"%" SVf "\"",
365 0           offset, SVfARG(self->shp_filename));
366             }
367              
368 4           record = NULL;
369              
370 4 50         if (shp_read_record(fh, &record) < 0) {
371 0           croak("Error reading \"%" SVf "\": %s",
372 0           SVfARG(self->shp_filename), fh->error);
373             }
374              
375 4 50         if (record != NULL) {
376 4 50         if (record->shape_type == SHPT_POLYGON) {
377 4           polygon = &record->shape.polygon;
378 4 100         if (shp_polygon_point_in_polygon(polygon, location) != 0) {
379 2           av_push(time_zones, SvREFCNT_inc(entry->time_zone));
380             }
381             }
382 4           free(record);
383             }
384             }
385             }
386              
387             MODULE = Geo::Location::TimeZoneFinder PACKAGE = Geo::Location::TimeZoneFinder
388              
389             PROTOTYPES: DISABLE
390              
391             TYPEMAP: <
392             Geo::Location::TimeZoneFinder T_PTROBJ
393             HERE
394              
395             SV *
396             new(klass, ...)
397             SV *klass
398             INIT:
399             Geo__Location__TimeZoneFinder self;
400 3           SV *file_base = NULL;
401             I32 i;
402             const char *key;
403             SV *value;
404             dbf_file_t *dbf_fh;
405             shp_file_t *shp_fh;
406             size_t expected_size;
407             CODE:
408 3           dXCPT;
409              
410 3 50         if ((items - 1) % 2 != 0) {
411 0           warn("Odd-length list passed to %" SVf " constructor", SVfARG(klass));
412             }
413              
414 5 100         for (i = 1; i < items; i += 2) {
415 2 50         key = SvPV_nolen_const(ST(i));
416 2           value = ST(i + 1);
417 2 50         if (strEQ(key, "file_base")) {
418 2           file_base = value;
419             }
420             }
421              
422 3 100         if (file_base == NULL) {
423 1           croak("The \"file_base\" parameter is mandatory");
424             }
425              
426 2           Newxz(self, 1, struct geo_location_timezonefinder);
427             storeTHX(self->perl);
428              
429 3 100         XCPT_TRY_START {
430 2           self->dbf_filename = newSVsv(file_base);
431 2           sv_catpvs(self->dbf_filename, ".dbf");
432 2           self->shp_filename = newSVsv(file_base);
433 2           sv_catpvs(self->shp_filename, ".shp");
434              
435             /* Open the database file with the time zones. */
436 2 50         self->dbf_fp = fopen(SvPV_nolen_const(self->dbf_filename), "rb");
437 2 100         if (self->dbf_fp == NULL) {
438 1           croak("Error opening \"%" SVf "\"", SVfARG(self->dbf_filename));
439             }
440              
441             /* Open the main file with the shapes. */
442 1 50         self->shp_fp = fopen(SvPV_nolen_const(self->shp_filename), "rb");
443 1 50         if (self->shp_fp == NULL) {
444 0           croak("Error opening \"%" SVf "\"", SVfARG(self->shp_filename));
445             }
446              
447             /* Read the time zones. */
448 1           dbf_fh = dbf_file(&self->dbf_fh, self->dbf_fp, self);
449 1 50         if (dbf_read(dbf_fh, handle_dbf_header, handle_dbf_record) < 0) {
450 0           croak("Error reading \"%" SVf "\": %s",
451 0           SVfARG(self->dbf_filename), dbf_fh->error);
452             }
453              
454             /* The time zone file is no longer needed. */
455 1           (void) fclose(self->dbf_fp);
456 1           self->dbf_fp = NULL;
457              
458 1           expected_size = self->index.num_entries;
459              
460 1 50         if (self->dbf_num != expected_size) {
461 0           croak("Expected %zu records, got %zu in \"%" SVf "\"",
462 0           expected_size, self->dbf_num, SVfARG(self->dbf_filename));
463             }
464              
465             /* Index the shapes by their bounding boxes. */
466 1           shp_fh = shp_file(&self->shp_fh, self->shp_fp, self);
467 1 50         if (shp_read(shp_fh, handle_shp_header, handle_shp_record) < 0) {
468 0           croak("Error reading \"%" SVf "\": %s",
469 0           SVfARG(self->shp_filename), shp_fh->error);
470             }
471              
472 1 50         if (self->shp_num != expected_size) {
473 0           croak("Expected %zu records, got %zu in \"%" SVf "\"",
474 0           expected_size, self->shp_num, SVfARG(self->shp_filename));
475             }
476 2           } XCPT_TRY_END
477              
478 2 100         XCPT_CATCH {
479 1           free_self(self);
480 1 50         XCPT_RETHROW;
    0          
481             }
482              
483 1           RETVAL = sv_bless(newRV_noinc(newSViv(PTR2IV(self))),
484             gv_stashsv(klass, GV_ADD));
485             OUTPUT:
486             RETVAL
487              
488             void
489             time_zones_at(self, ...)
490             Geo::Location::TimeZoneFinder self
491             ALIAS:
492             time_zone_at = 1
493             INIT:
494 20           SV *latitude = NULL;
495 20           SV *longitude = NULL;
496 20           NV lat = 0.0;
497 20           NV lon = 0.0;
498             I32 i;
499             const char *key;
500             SV *value;
501             shp_point_t location;
502             AV *time_zones;
503             SSize_t tz_count, tz_num;
504             SV **svp;
505 20 50         U8 gimme = GIMME_V;
506             PPCODE:
507 20 50         if ((items - 1) % 2 != 0) {
508 0 0         warn("Odd-length list passed to %s method",
509             (ix == 1) ? "time_zone_at" : "time_zones_at");
510             }
511              
512 58 100         for (i = 1; i < items; i += 2) {
513 38 50         key = SvPV_nolen_const(ST(i));
514 38           value = ST(i + 1);
515 38 100         if (strEQ(key, "lat") || strEQ(key, "latitude")) {
    100          
516 19           latitude = value;
517             }
518 19 100         else if (strEQ(key, "lon") || strEQ(key, "longitude")) {
    50          
519 19           longitude = value;
520             }
521             }
522              
523 20 100         if (latitude == NULL) {
524 1           croak("The \"latitude\" parameter is mandatory");
525             }
526              
527 19 100         if (longitude == NULL) {
528 1           croak("The \"longitude\" parameter is mandatory");
529             }
530              
531 18 100         if (!SvNIOK(latitude)) {
532 2           croak("The \"latitude\" parameter %" SVf " is not a number between "
533             "-90 and 90", SVfARG(latitude));
534             }
535              
536 16 100         if (!SvNIOK(longitude)) {
537 2           croak("The \"longitude\" parameter %" SVf " is not a number between "
538             "-180 and 180", SVfARG(longitude));
539             }
540              
541 14 100         lat = SvNV(latitude);
542 14 100         lon = SvNV(longitude);
543              
544 14 50         if (Perl_isnan(lat) || lat < -90.0 || lat > 90.0) {
    100          
    100          
545 2           croak("The \"latitude\" parameter %" SVf " is not a number between "
546             "-90 and 90", SVfARG(latitude));
547             }
548              
549 12 50         if (Perl_isnan(lon) || lon < -180.0 || lon > 180.0) {
    100          
    100          
550 2           croak("The \"longitude\" parameter %" SVf " is not a number between "
551             "-180 and 180", SVfARG(longitude));
552             }
553              
554 10           time_zones = newAV();
555              
556 10           location.x = lon;
557 10           location.y = lat;
558 10           get_special_time_zones(self, &location, time_zones);
559 10 100         if (av_len(time_zones) < 0) {
560 7           get_time_zones(self, &location, time_zones);
561 7 100         if (av_len(time_zones) < 0) {
562 4           get_time_zones_at_sea(self, &location, time_zones);
563             }
564             }
565              
566 10           tz_count = av_len(time_zones) + 1;
567 47 100         for (tz_num = 0; tz_num < tz_count; ++tz_num) {
568 38           svp = av_fetch(time_zones, tz_num, 0);
569 38 50         if (svp != NULL) {
570 38 50         XPUSHs(SvREFCNT_inc(*svp));
571 38 100         if (gimme == G_SCALAR || ix == 1) {
    50          
572             break;
573             }
574             }
575             }
576              
577 10           SvREFCNT_dec((SV *) time_zones);
578              
579             SV *
580             index(self)
581             Geo::Location::TimeZoneFinder self
582             INIT:
583             AV *results, *box;
584             struct index *index;
585             struct index_entry *entry;
586             size_t n, i;
587             HV *rh;
588             CODE:
589 1           results = (AV *) sv_2mortal((SV *) newAV());
590 1           index = &self->index;
591 1           n = index->num_entries;
592 7 100         for (i = 0; i < n; ++i) {
593 6           entry = &index->entries[i];
594 6           rh = (HV *) sv_2mortal((SV *) newHV());
595 6           box = (AV *) sv_2mortal((SV *) newAV());
596 6           av_extend(box, 3);
597 6           av_push(box, newSVnv(entry->box.x_min));
598 6           av_push(box, newSVnv(entry->box.y_min));
599 6           av_push(box, newSVnv(entry->box.x_max));
600 6           av_push(box, newSVnv(entry->box.y_max));
601 6           hv_store(rh, "bounding_box", 12, newRV_inc((SV *) box), 0);
602 6           hv_store(rh, "file_offset", 11, newSViv(entry->file_offset), 0);
603 6           hv_store(rh, "time_zone", 9, SvREFCNT_inc(entry->time_zone), 0);
604 6           av_push(results, newRV_inc((SV *) rh));
605             }
606 1           RETVAL = newRV_inc((SV *) results);
607             OUTPUT:
608             RETVAL
609              
610             void
611             DESTROY(self)
612             Geo::Location::TimeZoneFinder self;
613             CODE:
614 1           free_self(self);