File Coverage

TimeZoneFinder.xs
Criterion Covered Total %
statement 219 245 89.3
branch 120 162 74.0
condition n/a
subroutine n/a
pod n/a
total 339 407 83.2


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