summaryrefslogtreecommitdiffstats
path: root/location.inc
blob: 567f5d5d67c36454d61234db5c41764b5f9931cc (plain)
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
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
<?php

/**
 * @file
 * Public API for the Location module.
 *
 * @defgroup Location Location: An API for working with locations.
 */


/**
 * @file
 * An implementation of a universal API for location manipulation.  Provides functions for
 * postal_code proximity searching, deep-linking into online mapping services.  Currently,
 * some options are configured through an interface provided by location.module.
 */

include_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'location') . '/earth.inc';

/**
 * Get a deep-link to a mapping service such as Yahoo! Maps or MapPoint given an location.
 *
 * The call is delegated based on the 'country' value in the $location parameter.
 *
 * @param array $location
 *   An associative array where
 *      'street'       => A string representing the street location
 *      'additional'   => A string for any additional portion of the street location
 *      'city'         => A string for the city name
 *      'province'     => The standard postal abbreviation for the province
 *      'country'      => The two-letter ISO code for the country of the location (REQUIRED)
 *      'postal_code'  => The international postal code for the location
 *
 * @param string $link_text
 *   Text for a link.
 *
 * @return string
 *   A link to a map provided by a third-party.  The idea is to encode the appropriate
 *   parameters as HTTP GET variables to the URL.
 *
 * @ingroup Location
 */
function location_map_link($location = array(), $link_text = 'See map: ') {
  if (!isset($location['country']) || $location['country'] == 'xx') {
    return '';
  }

  location_load_country($location['country']);

  $default_func = 'location_map_link_' . $location['country'] . '_default_providers';
  $providers_func = 'location_map_link_' . $location['country'] . '_providers';
  // Merge the global providers with country specific ones so that countries
  // can add to or override the defaults.
  $providers = function_exists($providers_func) ? array_merge(
    location_map_link_providers(),
    $providers_func()
  ) : location_map_link_providers();
  // Default providers will be taken from the country specific default providers
  // function if it exists, otherwise it will use the global function.
  $selected_providers = variable_get(
    'location_map_link_' . $location['country'],
    function_exists($default_func) ? $default_func() : location_map_link_default_providers()
  );

  $links = array();
  foreach ($selected_providers as $mapper) {
    $link_func = 'location_map_link_' . $location['country'] . '_' . $mapper;
    // If there is no country function try for a global one.
    $link_func = function_exists($link_func) ? $link_func : 'location_map_link_' . $mapper;
    if (function_exists($link_func)) {
      if ($link = $link_func($location)) {
        $links[] = '<a href="' . $link . '"' . (variable_get('location_maplink_external', 0) ? ' ' . variable_get(
              'location_maplink_external_method',
              'target="_blank"'
            ) : '') . '>' . $providers[$mapper]['name'] . '</a>';
      }
    }
  }
  if (count($links)) {
    return '<div class="location map-link">' . t($link_text) . implode($links, ", ") . '</div>';
  }
  else {
    return NULL;
  }
}

/**
 * Provide default map link providers.
 *
 * By implementing location_map_link_COUNTRYCODE_providers, individual
 * countries can add to this (they will be merged so this default can also be
 * overridden using those functions).
 *
 * @return array
 *   An array where
 *    - the key is the a descriptive key for the provider.
 *      The key is also used in the function name of the function that
 *      generates the map links for that provider. For example, a key of
 *      'google' means the name of the function that builds a link to a map
 *      Google Maps would be 'location_map_link_google', or
 *      'location_map_link_COUNTRYCODE_google' for country overrides.
 *    - the value is itself an array with 3 key/value pairs:
 *      - 'name' => points to the name of the mapping service.  For 'google',
 *        this would be 'Google Maps'
 *      - 'url' => the url of the main page of the mapping service.  For
 *        'google', this would be 'http://maps.google.com'
 *      - 'tos' => the url of the page that explains the map providers Terms of
 *        Service, or Terms of Use. For 'google', this would be
 *        'http://www.google.com/help/terms_local.html'
 */
function location_map_link_providers() {
  return array(
    'google' => array(
      'name' => t('Google Maps'),
      'url' => 'http://maps.google.com',
      'tos' => 'http://www.google.com/help/terms_local.html',
    ),
  );
}

/**
 * Provide the default map link providers.
 *
 * This can be overridden by implementing
 * location_map_link_COUNTRYCODE_default_providers for a given country.
 *
 * @return array
 *   An array of values that work as keys to the array returned by
 *   location_map_link_providers (and country versions of that function).
 *   The idea is that if the administrator of the site has not yet had a chance
 *   to visit the "Map Links" subtab on the location module's settings page,
 *   that we can provide deep-linking to a relatively safe default.
 *   By 'relatively safe', we mean that the Terms Of Service of the provider
 *   of the maps are flexible enough for most parties.
 *
 *   For example, in the case of the U.S., 'google' has relatively flexible
 *   Terms Of Service, whereas Yahoo! Maps and MapQuest have more restrictive
 *   Terms Of Service.
 */
function location_map_link_default_providers() {
  return array('google');
}

/**
 * Google link.
 */
function location_map_link_google($location = array()) {
  $query_params = array();

  foreach (array('street', 'city', 'province', 'postal_code', 'country') as $field) {
    if (isset($location[$field])) {
      $query_params[] = $location[$field];
    }
  }

  if (count($query_params)) {
    return ('http://maps.google.com?q=' . urlencode(implode(", ", $query_params)));
  }
  else {
    return NULL;
  }
}

/**
 * Try to extract the the Latitude and Longitude data from the postal code.
 *
 * @param array $location
 *   Array. the location data
 *   -> the values are:
 *     'street'         => the string representing the street location (REQUIRED)
 *     'additional'     => the string representing the additional street location portion in the location form
 *     'city'           => the city name (REQUIRED)
 *     'province'       => the province code defined in the country-specific include file
 *     'country'        => the lower-case of the two-letter ISO code (REQUIRED)
 *     'postal_code'    => the postal-code (REQUIRED)
 *
 * @return array|null
 *   Array or NULL. NULL if the delegated-to function that does the
 *   actual look-up does not exist. If the appropriate function exists,
 *   then this function returns an associative array where
 *    'lon' => A floating point number for the longitude coordinate of the parameter location
 *    'lat' => A floating point number for the latitude coordinate of the parameter location
 *
 * @ingroup Location
 */
function location_get_postalcode_data($location = array()) {
  $location['country'] = isset($location['country']) ? trim($location['country']) : NULL;
  $location['postal_code'] = isset($location['postal_code']) ? trim($location['postal_code']) : NULL;
  if (is_null($location['postal_code']) || is_null(
      $location['country']
    ) || empty($location['country']) || empty($location['postal_code']) || $location['postal_code'] == 'xx'
  ) {
    return NULL;
  }
  location_load_country($location['country']);
  $country_specific_function = 'location_get_postalcode_data_' . $location['country'];
  if (function_exists($country_specific_function)) {
    return $country_specific_function($location);
  }
  else {
    return NULL;
  }
}

/**
 * Given two points in lat/lon form, returns the distance between them.
 *
 * @param array $latlon_a
 *   An associative array where
 *      'lon' => is a floating point of the longitude coordinate for the point given by latlonA
 *      'lat' => is a floating point of the latitude coordinate for the point given by latlonB
 *
 * @param array $latlon_b
 *   Another point formatted like $latlon_b
 *
 * @param string $distance_unit
 *   A string that is either 'km' or 'mile'.
 *      If neither 'km' or 'mile' is passed, the parameter is forced to 'km'
 *
 * @return array|null
 *   NULL if sense can't be made of the parameters.
 *    An associative array where
 *      'scalar' => Is the distance between the two lat/lon parameter points
 *      'distance_unit' => Is the unit of distance being represented by 'scalar'.
 *                         This will be 'km' unless 'mile' is passed for the $distance_unit param
 *
 * @ingroup Location
 */
function location_distance_between($latlon_a = array(), $latlon_b = array(), $distance_unit = 'km') {
  if (!isset($latlon_a['lon']) || !isset($latlon_a['lat']) || !isset($latlon_b['lon']) || !isset($latlon_b['lat'])) {
    return NULL;
  }

  if ($distance_unit != 'km' && $distance_unit != 'mile') {
    return NULL;
  }

  // $conversion_factor = number to divide by to convert meters to $distance_unit
  // At this point, $distance_unit == 'km' or 'mile' and nothing else
  // $conversion_factor = ($distance_unit == 'km') ? 1000.0 : 1609.347;

  $meters = earth_distance($latlon_a['lon'], $latlon_a['lat'], $latlon_b['lon'], $latlon_b['lat']);

  return array(
    'scalar' => round($meters / (($distance_unit == 'km') ? 1000.0 : 1609.347), 1),
    'distance_unit' => $distance_unit,
  );
}

/**
 * Takes two locations and tries to return a deep-link to driving directions.
 *
 * @param array $location_a
 *   An associative array that represents an location where
 *      'street'       => the street portions of the location
 *      'additional'   => additional street portion of the location
 *      'city'         => the city name
 *      'province'     => the province, state, or territory
 *      'country'      => lower-cased two-letter ISO code (REQUIRED)
 *      'postal_code'  => the postal code
 *
 * @param array $location_b
 *   An associative array that represents an location in the same way that
 *   parameter $location_a does.
 *
 * @param string $link_text
 *   The text of the HTML link that is to be generated.
 *
 * @return string
 *   A deep-link to driving directions on Yahoo! or some other mapping service, if enough fields are filled in the parameters.
 *   A deep-link to a form for driving directions with information pre-filled if not enough, but some fields are filled in the parameters.
 *   The empty string if no information is provided (or if so little information is provided that there is no function to which to delegate
 *   the call.
 *
 *   We dispatch the call to a country-specific function.  The country-specific function, in this case,
 *   will be the one reflected by the country parameter of the first function.  We require that
 *   both locationes supplied have a country field at the minimum.
 *
 *   The country-specific functions will ultimately decide, with the parameters given, whether to
 *   to link to a form for driving directions is provided, where this form will be
 *   pre-populated with whatever values were available or whether to link directly to the driving
 *   directions themselves if enough fields are filled for each location.
 *
 * @ingroup Location
 */
function location_driving_directions_link($location_a = array(), $location_b = array(), $link_text = 'Get directions') {
  if (!isset($location_a['country']) or !isset($location_b['country'])) {
    return '';
  }

  // For now, return empty string if starting-point and destinations are in different countries
  // if ($location_a['country'] != $location_b['country']) {
  // return '';
  // }
  // Lines above commented out because I want to let the country-specific function of the departure point decide
  // what it will do with driving destination locationes from other countries.  As an example, Yahoo! Maps supports driving
  // direction queries for locations between the U.S. and Canada.

  $driving_direction_function = 'location_driving_directions_link_' . $location_a['country'];
  if (function_exists($driving_direction_function)) {
    $http_link = $driving_direction_function($location_a, $location_b);
    if (strlen($http_link)) {
      return '<a href="' . $http_link . '">' . $link_text . '</a>';
    }
    else {
      return '';
    }
  }

  return '';
}

/**
 * Convert distances to meter.
 *
 * @param int $distance
 *   A number in either miles or km.
 *
 * @param string $distance_unit
 *   String (optional). Either 'mile' or 'km' (default)
 *
 * @return float
 *   A floating point number where the number in meters after the initially passed scalar has been ceil()'d
 *   This is done after the $distance_unit parmeter is forced to be 'km' or 'mile'
 */
function _location_convert_distance_to_meters($distance, $distance_unit = 'km') {
  if (!is_numeric($distance)) {
    return NULL;
  }

  if ($distance == 0) {
    return NULL;
  }

  if ($distance_unit != 'km' && $distance_unit != 'mile') {
    $distance_unit = 'km';
  }

  // Convert distance to meters.
  $retval = round(floatval($distance) * (($distance_unit == 'km') ? 1000.0 : 1609.347), 2);

  return $retval;
}

/**
 * Takes an location and returns a "rough" latitude/longitude pair based on the postal code data available for the given country.
 *
 * @param array $location
 *   An associative array $location where
 *     'street'       => the street portion of the location
 *     'additional' => additional street portion of the location
 *     'province'     => the province, state, or territory
 *     'country'      => lower-cased two-letter ISO code (REQUIRED)
 *     'postal_code'  => international postal code (REQUIRED)
 *
 * @return array|null
 *   NULL if data cannot be found.
 *   Otherwise, an associative array where
 *     'lat' => is a floating point of the latitude coordinate of this location
 *     'lon' => is a floating point of the longitude coordinate of this location
 *
 * @ingroup Location
 */
function location_latlon_rough($location = array()) {
  if (!isset($location['country']) || !isset($location['postal_code'])) {
    return NULL;
  }

  location_load_country($location['country']);

  $latlon_function = 'location_latlon_rough_' . $location['country'];
  if (function_exists($latlon_function) && $result = $latlon_function($location)) {
    return $result;
  }
  else {
    return location_latlon_rough_default($location);
  }
}

/**
 * Returns a lat/lon pair of the approximate center of the given postal code in the given country.
 *
 * This function is a default implementation, in case the country support doesn't implement a proper function for this.
 *
 * @param array $location
 *   An associative array $location where
 *     'street'       => the street portion of the location
 *     'supplemental' => additional street portion of the location
 *     'province'     => the province, state, or territory
 *     'country'      => lower-cased two-letter ISO code (REQUIRED)
 *     'postal_code'  => the international postal code for this location (REQUIRED)
 *
 * @return array
 *   An associative array where
 *      'lat' => approximate latitude of the center of the postal code's area
 *      'lon' => approximate longitude of the center of the postal code's area
 */
function location_latlon_rough_default($location = array()) {
  if (!isset($location['country']) || !isset($location['postal_code'])) {
    return NULL;
  }

  $query = db_select('zipcodes', 'z');
  $query->addField('z', 'latitude', 'lat');
  $query->addField('z', 'longitude', 'lon');
  $coords = $query->condition('country', $location['country'])
    ->condition('zip', $location['postal_code'])
    ->execute()
    ->fetchAssoc();

  if ($coords) {
    return $coords;
  }
  // If nothing found internally (new zipcode, or incomplete zipcodes table).
  elseif (($newlatlong = location_latlon_exact($location)) != NULL) {
    // Try a one-time external geocoding.
    if ($newlatlong['lat']) {
      // And store results in zipcodes table for next lookups being internally handled
      // (yeah this is missing city/state info a.t.m., but is way better than nothing!)
      db_insert('zipcodes')
        ->fields(
          array(
            'latitude' => $newlatlong['lat'],
            'longitude' => $newlatlong['lon'],
            'country' => $location['country'],
            'zip' => $location['postal_code'],
          )
        )
        ->execute();
    }

    return $newlatlong;
  }
  else {
    return NULL;
  }
}

/**
 * Get an appropriate bounding box for showing an entire country on a map.
 *
 * Target is a bounding box large enough to show the country in both spherical
 * mercator and mercator projections.
 *
 * @param array $location
 *   Either a location array or a country code.
 *
 * @return array
 *   An array with 'minlng', 'minlat', 'maxlng', and 'maxlat' elements.
 */
function location_country_bounds($location = array()) {
  if (!is_array($location)) {
    $location = array('country' => $location);
  }
  if (!empty($location['country'])) {
    location_load_country($location['country']);
  }

  $country_bounds_function = 'location_bounds_' . $location['country'];

  if (function_exists($country_bounds_function)) {
    return $country_bounds_function();
  }
  else {
    return array(
      'minlng' => -180.0,
      'minlat' => -90.0,
      'maxlng' => 180.0,
      'maxlat' => 90.0,
    );
  }
}

/**
 * Currently, this is not a priority until there is an implementable use for exact longitude latitude coordinates for an location.
 *
 * The idea is that this call will eventually retrieve
 * information through a web-service.  Whereas location_latlon_rough() returns an approximate
 * lat/lon pair based strictly on the postal code where this lat/lon pair is pulled from a
 * database table, this function is intended to send the entire location to a web-service and
 * to retrieve exact lat/lon coordinates.
 *
 * @param array $location
 *   An array where
 *     -> the key values are 'street', 'additional', 'province', 'country', 'postal_code'
 *     -> the values are:
 *         'street'         => the string representing the street location (REQUIRED)
 *         'additional'     => the string representing the additional street location portion in the location form
 *         'city'           => the city name (REQUIRED)
 *         'province'       => the province code defined in the country-specific include file
 *         'country'        => the lower-case of the two-letter ISO code (REQUIRED)
 *         'postal_code'    => the postal-code (REQUIRED)
 *
 * @return array|null
 *   NULL if the delegated-to function that does the actual look-up does not exist.
 *   If the appropriate function exists, then this function returns an associative array where
 *      'lon' => A floating point number for the longitude coordinate of the parameter location
 *      'lat' => A floating point number for the latitude coordinate of the parameter location
 *
 * @ingroup Location
 */
function location_latlon_exact($location = array()) {
  $country = $location['country'];
  location_standardize_country_code($country);
  $service = variable_get('location_geocode_' . $country, 'none');
  if (!empty($country) && $service != 'none') {
    // Figure out what the exact function should be.
    if (strpos($service, '|')) {
      location_load_country($country);
      // The code change below fixes the problem of the country specific
      // function for geocoding not being correctly called (it removes any
      // text from the pipe (|) onwards).
      $exact_latlon_function = 'location_geocode_' . $country . '_' . substr($service, 0, strpos($service, '|'));
    }
    else {
      location_load_geocoder($service);
      $exact_latlon_function = $service . '_geocode_location';
    }
    if (function_exists($exact_latlon_function)) {
      return $exact_latlon_function($location);
    }
    else {
      return NULL;
    }
  }

  return NULL;
}

/**
 * Returns an associative array of countries currently supported by the location system where.
 *
 * -> the keys represent the two-letter ISO code and
 * -> the values represent the English name of the country.
 * The array is sorted the index (i.e., by the short English name of the country).
 *
 * Please note the different between "supported" countries and "configured"
 * countries: A country being "supported" means that there is an include file
 * to support the country while "configure" implies that the site admin has
 * configured the site to actually use that country.
 *
 * @ingroup Location
 */
function _location_supported_countries() {
  $supported_countries = & drupal_static(__FUNCTION__, array());

  // If this function has already been called this request, we can avoid a DB hit.
  if (!empty($supported_countries)) {
    return $supported_countries;
  }

  // Try first to load from cache, it's much faster than the scan below.
  if ($cache = cache_get('location:supported-countries', 'cache_location')) {
    $supported_countries = $cache->data;
  }
  else {
    // '<ISO two-letter code>' => '<English name for country>'
    $iso_list = location_get_iso3166_list();
    $path = drupal_get_path('module', 'location') . '/supported/location.';
    foreach ($iso_list as $cc => $name) {
      if (file_exists($path . $cc . '.inc')) {
        $supported_countries[$cc] = $name;
      }
    }
    cache_set('location:supported-countries', $supported_countries, 'cache_location');
  }

  return $supported_countries;
}

// @@@ New in 3.x, document.
/**
 * Fetch the provinces for a country.
 */
function location_get_provinces($country = 'us') {
  $provinces = & drupal_static(__FUNCTION__, array());
  // Current language.
  $lang_code = $GLOBALS['language']->language;

  location_standardize_country_code($country);
  if (!isset($provinces[$country])) {
    if ($cache = cache_get("provinces:$country:$lang_code", 'cache_location')) {
      $provinces[$country] = $cache->data;
    }
    else {
      location_load_country($country);
      $func = 'location_province_list_' . $country;
      if (function_exists($func)) {
        $provinces[$country] = $func();
        cache_set("provinces:$country:$lang_code", $provinces[$country], 'cache_location');
      }
    }
  }

  // Invoke hook_location_provinces_alter and
  // hook_location_provinces_COUNTRY_CODE_alter.
  drupal_alter(
    array('location_provinces', 'location_provinces_' . $country),
    $provinces,
    $country
  );

  return isset($provinces[$country]) ? $provinces[$country] : array();
}

// @@@ New in 3.x, document.
/**
 * Get the translated name of a country code.
 */
function location_country_name($country = 'us') {
  location_standardize_country_code($country);
  $countries = location_get_iso3166_list();
  if (isset($countries[$country])) {
    return $countries[$country];
  }
  else {
    return '';
  }
}

// @@@ New in 3.x, document.
/**
 * Get the full name of a province code.
 */
function location_province_name($country = 'us', $province = 'xx') {
  $provinces = location_get_provinces($country);
  $province = strtoupper($province);
  if (isset($provinces[$province])) {
    return $provinces[$province];
  }
  else {
    // If the province value is not indexed, allow pass-through as-is.
    // This helps with legacy data migration that may not be stored
    // in the correct provincial code format.
    return check_plain($province);
  }
}

// @@@ New in 3.x, document.
/**
 * Get a province code from a code or full name and a country.
 */
function location_province_code($country = 'us', $province = 'xx') {
  // An array of countries is useful if someone specified multiple countries
  // in an autoselect for example.
  // It *is* possibly ambiguous, especially if the province was already a code.
  // We make an array here for single (the usual case) for code simplicity reasons.
  if (!is_array($country)) {
    $country = array($country);
  }

  $p = strtoupper($province);
  foreach ($country as $c) {
    if ($c == 'xx') {
      return $province;
    }
    $provinces = location_get_provinces($c);
    foreach ($provinces as $k => $v) {
      if ($p == strtoupper($k) || $p == strtoupper($v)) {
        return $k;
      }
    }
  }

  return '';
}

// @@@ New in 3.x, document.
/**
 * Canonicalize a country code.
 */
function location_standardize_country_code(&$country) {
  $country = trim($country);
  // @@@ Double check the validity of this validity check. ;)
  if (!ctype_alpha($country) || strlen($country) != 2) {
    $country = 'xx';

    return FALSE;
  }
  else {
    $country = strtolower($country);

    return TRUE;
  }
}

/**
 * Load support for a country.
 *
 * This function will load support for a country identified by its two-letter ISO code.
 *
 * @param string $country
 *   Two-letter ISO code for country.
 *
 * @return bool
 *   TRUE if the file was found and loaded, FALSE otherwise.
 */
function location_load_country($country) {
  location_standardize_country_code($country);

  $file = DRUPAL_ROOT . '/' . drupal_get_path('module', 'location') . '/supported/location.' . $country . '.inc';
  if (file_exists($file)) {
    include_once $file;

    return TRUE;
  }

  return FALSE;
}

// @@@ New in 3.x, document.
/**
 * Load a general geocoding service.
 */
function location_load_geocoder($geocoder) {
  include_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'location') . '/geocoding/' . $geocoder . '.inc';
}

/**
 * Create a single line address.
 *
 * @param array $location
 *   The address parts
 *
 * @return string
 *   The single line address
 */
function location_address2singleline($location = array()) {
  // Check if its a valid address.
  if (empty($location)) {
    return '';
  }

  $address = '';
  if (!empty($location['street'])) {
    $address .= $location['street'];
  }

  if (!empty($location['city'])) {
    if (!empty($location['street'])) {
      $address .= ', ';
    }

    $address .= $location['city'];
  }

  if (!empty($location['province'])) {
    if (!empty($location['street']) || !empty($location['city'])) {
      $address .= ', ';
    }

    // @@@ Fix this!
    if (substr($location['province'], 0, 3) == $location['country'] . '-') {
      $address .= substr($location['province'], 3);
      watchdog('Location', 'BUG: Country found in province attribute.');
    }
    else {
      $address .= $location['province'];
    }
  }

  if (!empty($location['postal_code'])) {
    if (!empty($address)) {
      $address .= ' ';
    }
    $address .= $location['postal_code'];
  }

  if (!empty($location['country'])) {
    $address .= ', ' . $location['country'];
  }

  return $address;
}

/**
 * Geocoder list.
 */
function location_get_general_geocoder_list() {
  $list = & drupal_static(__FUNCTION__, array());

  if (!count($list)) {
    $files = file_scan_directory(
      drupal_get_path('module', 'location') . '/geocoding',
      '/\.inc$/',
      array('nomask' => '/(\.\.?|CVS|\.svn)$/')
    );
    foreach ($files as $full_path_name => $fileinfo) {
      $list[] = $fileinfo->name;
    }
  }

  return $list;
}

/**
 * The following is an array of all.
 *
 * countrycode => country-name pairs as layed out in
 * ISO 3166-1 alpha-2
 */
function location_get_iso3166_list($upper = FALSE) {
  include_once DRUPAL_ROOT . '/includes/locale.inc';

  // Statically cache a version of the core Drupal list of countries
  // with lower case country codes for use by this module.
  $countries = & drupal_static(__FUNCTION__);

  if ($upper) {
    // Drupal core stores ISO 3166-1 alpha2 codes in upper case, as
    // per the ISO standard.
    return country_get_list();
  }
  elseif (!isset($countries)) {
    // Location module uses lower-case ISO 3166-1 alpha2 codes, so we need
    // to convert.
    $countries = array_change_key_case(country_get_list(), CASE_LOWER);
  }

  return $countries;
}