Skip to content

Commit b3a34d3

Browse files
cpaulgilmansjanzou
andauthored
Migrate Bing Maps API to Azure Maps API (#2093)
* Initial migration from Bing to Azure Maps for time zone API service * Replace Bing Maps API with Azure Map API * Make time zone optional for geocode LK function * Update 3D shade calculator map underlay * Convert Bing Map to Azure Map * Avoid crash if map underlay fails * Improve user feedback if map underlay fails * Remove second call to geocode function * Update geocode LK_DOC string * Fix ambiguous variable name * Fix return error message for GeoTools::StaticMap on GitHub Actions * Initialize lat, lon, tz to NaN for LK geocode function * Remove outdated comment --------- Co-authored-by: Steven Janzou <steven@janzouconsulting.com>
1 parent 4fb96f3 commit b3a34d3

File tree

8 files changed

+151
-107
lines changed

8 files changed

+151
-107
lines changed

deploy/runtime/ui/Solar Resource Data.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3841,8 +3841,6 @@
38413841
"\t\t{\r",
38423842
"\t\t\tlocation = loc_test.address;\r",
38433843
"\t\t\tg = geocode( location );\r",
3844-
"\t\t\t// sometimes correct address fails but works on second try\r",
3845-
"\t\t\tif ( !g.ok ) { g = geocode( location ); }\r",
38463844
"\t\t\tif ( g.ok ) \r",
38473845
"\t\t\t{\r",
38483846
"\t\t\t\tlat = g.lat;\r",

src/geotools.cpp

Lines changed: 97 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,6 @@ bool GeoTools::GeocodeDeveloper(const wxString& address, double* lat, double* lo
193193
curl.AddHttpHeader("Content-Type: application/json");
194194
curl.AddHttpHeader("Accept: application/json");
195195

196-
197196
if (showprogress) {
198197
if (!curl.Get(url, "Geocoding address '" + address + "'..."))
199198
return false;
@@ -203,27 +202,12 @@ bool GeoTools::GeocodeDeveloper(const wxString& address, double* lat, double* lo
203202
return false;
204203
}
205204

206-
// change from UTF8 to UTF16 encoding to address unicode characters per SAM GitHub issue 1848
205+
// change from UTF8 to UTF16 encoding to address unicode characters per SAM issue 1848
207206
rapidjson::GenericDocument < rapidjson::UTF16<> > reader;
208207
wxString str = curl.GetDataAsString();
209208

210209
rapidjson::ParseResult ok = reader.Parse(str.c_str());
211210

212-
213-
/*
214-
example for "denver, co"
215-
{"info":
216-
{"statuscode":0,"copyright":
217-
{"text":"© 2024 MapQuest, Inc.","imageUrl":"http://api.mqcdn.com/res/mqlogo.gif","imageAltText":"© 2024 MapQuest, Inc."}
218-
,"messages":[]}
219-
,"options":{"maxResults":-1,"ignoreLatLngInput":false}
220-
,"results":
221-
[{"providedLocation":{"location":"denver co"},"locations":
222-
[{"street":"","adminArea6":"","adminArea6Type":"Neighborhood","adminArea5":"Denver","adminArea5Type":"City","adminArea4":"Denver","adminArea4Type":"County","adminArea3":"CO","adminArea3Type":"State","adminArea1":"US","adminArea1Type":"Country","postalCode":"","geocodeQualityCode":"A5XAX","geocodeQuality":"CITY","dragPoint":false,"sideOfStreet":"N","linkId":"0","unknownInput":"","type":"s","latLng":{"lat":39.74001,"lng":-104.99202},"displayLatLng":{"lat":39.74001,"lng":-104.99202},"mapUrl":""}]
223-
}
224-
]}
225-
*/
226-
227211
if (!reader.HasParseError()) {
228212
if (reader.HasMember(L"results")) {
229213
if (reader[L"results"].IsArray()) {
@@ -233,21 +217,21 @@ bool GeoTools::GeocodeDeveloper(const wxString& address, double* lat, double* lo
233217
if (reader[L"results"][0][L"locations"][0][L"latLng"].HasMember(L"lat")) {
234218
if (reader[L"results"][0][L"locations"][0][L"latLng"][L"lat"].IsNumber()) {
235219
*lat = reader[L"results"][0][L"locations"][0][L"latLng"][L"lat"].GetDouble();
236-
success = true;
237-
}
238-
}
239-
if (reader[L"results"][0][L"locations"][0][L"latLng"].HasMember(L"lng")) {
240-
if (reader[L"results"][0][L"locations"][0][L"latLng"][L"lng"].IsNumber()) {
241-
*lon = reader[L"results"][0][L"locations"][0][L"latLng"][L"lng"].GetDouble();
242-
success &= true;
220+
if (reader[L"results"][0][L"locations"][0][L"latLng"].HasMember(L"lng")) {
221+
if (reader[L"results"][0][L"locations"][0][L"latLng"][L"lng"].IsNumber()) {
222+
*lon = reader[L"results"][0][L"locations"][0][L"latLng"][L"lng"].GetDouble();
223+
success = true; // only if lat and lon are numbers
224+
}
225+
}
226+
243227
}
244228
}
245-
}
229+
}
246230
}
247231
}
248232
}
249233
}
250-
// check status code
234+
/* // check status code
251235
success = false;//overrides success of retrieving data
252236
253237
if (reader.HasMember(L"info")) {
@@ -257,6 +241,7 @@ bool GeoTools::GeocodeDeveloper(const wxString& address, double* lat, double* lo
257241
}
258242
}
259243
}
244+
*/
260245
}
261246
else {
262247
wxMessageBox(rapidjson::GetParseError_En(ok.Code()), "geocode developer parse error ");
@@ -265,83 +250,86 @@ bool GeoTools::GeocodeDeveloper(const wxString& address, double* lat, double* lo
265250
if (!success)
266251
return false;
267252

268-
269-
253+
// time zone is optional
270254
if (tz != 0)
271255
{
272256
success = false;
273257

274258
curl = wxEasyCurl();
275259

276-
277-
278-
url = SamApp::WebApi("bing_maps_timezone_api");
279-
url.Replace("<POINT>", wxString::Format("%.14lf,%.14lf", *lat, *lon));
280-
url.Replace("<BINGAPIKEY>", wxString(bing_api_key));
260+
// azure maps time zone api
261+
url = SamApp::WebApi("azure_maps_timezone_api");
262+
url.Replace("<LATLON>", wxString::Format("%.14lf,%.14lf", *lat, *lon));
263+
url.Replace("<AZUREAPIKEY>", wxString(azure_api_key));
281264

282265
curl.AddHttpHeader("Content-Type: application/json");
283266
curl.AddHttpHeader("Accept: application/json");
284267

285-
286-
if (showprogress)
287-
{
288-
if (!curl.Get(url, "Geocoding address '" + address + "'..."))
289-
return false;
268+
if (showprogress) {
269+
curl.Get(url, "Getting time zone '" + address + "'...");
290270
}
291271
else {
292-
if (!curl.Get(url))
293-
return false;
272+
curl.Get(url);
294273
}
295274

296275
str = curl.GetDataAsString();
297276

298277
reader.Parse(str.c_str());
299278

279+
if (reader.HasMember(L"error")) {
280+
if (reader[L"error"].HasMember(L"code")) {
281+
if (reader[L"error"][L"code"].IsString()) {
282+
wxString error_str = reader[L"error"][L"code"].GetString();
283+
if (error_str.Lower() != "") {
284+
wxMessageBox(wxString::Format("Time Zone API Error!\n%s", error_str));
285+
return false;
286+
}
287+
}
288+
}
289+
}
290+
291+
*tz = NULL;
300292
if (!reader.HasParseError()) {
301-
if (reader.HasMember(L"resourceSets")) {
302-
if (reader[L"resourceSets"].IsArray()) {
303-
if (reader[L"resourceSets"][0].HasMember(L"resources")) {
304-
if (reader[L"resourceSets"][0][L"resources"].IsArray()) {
305-
if (reader[L"resourceSets"][0][L"resources"][0].HasMember(L"timeZone")) {
306-
if (reader[L"resourceSets"][0][L"resources"][0][L"timeZone"].HasMember(L"utcOffset")) {
307-
if (reader[L"resourceSets"][0][L"resources"][0][L"timeZone"][L"utcOffset"].IsString()) {
308-
wxString stz = reader[L"resourceSets"][0][L"resources"][0][L"timeZone"][L"utcOffset"].GetString();
309-
wxArrayString as = wxSplit(stz, ':');
310-
if (as.Count() != 2) return false;
311-
if (!as[0].ToDouble(tz)) return false;
312-
double offset = 0;
313-
if (as[1] == "30") offset = 0.5;
314-
if (*tz < 0)
315-
*tz = *tz - offset;
316-
else
317-
*tz = *tz + offset;
318-
success = true;
319-
}
320-
}
293+
if (reader.HasMember(L"TimeZones")) {
294+
if (reader[L"TimeZones"].IsArray()) {
295+
if (reader[L"TimeZones"][0].HasMember(L"ReferenceTime")) {
296+
if (reader[L"TimeZones"][0][L"ReferenceTime"].HasMember(L"StandardOffset")) {
297+
if (reader[L"TimeZones"][0][L"ReferenceTime"][L"StandardOffset"].IsString()) {
298+
wxString stz = reader[L"TimeZones"][0][L"ReferenceTime"][L"StandardOffset"].GetString();
299+
wxArrayString as = wxSplit(stz, ':');
300+
if (as.Count() != 3) return false; // example "-08:00:00"
301+
if (!as[0].ToDouble(tz)) return false;
302+
double offset = 0;
303+
if (as[1] == "30") offset = 0.5;
304+
if (*tz < 0)
305+
*tz = *tz - offset;
306+
else
307+
*tz = *tz + offset;
308+
success = true;
321309
}
322310
}
323311
}
324312
}
325313
}
326-
// check status code
327-
success = false;//overrides success of retrieving data
328-
329-
if (reader.HasMember(L"statusDescription")) {
330-
if (reader[L"statusDescription"].IsString()) {
331-
wxString str = reader[L"statusDescription"].GetString();
332-
success = str.Lower() == "ok";
333-
}
334-
}
335314
}
336-
315+
else { // parse error
316+
wxMessageBox(wxString::Format("Time Zone API Error!\nFailed to parse response."));
317+
success = false;
318+
}
337319
}
338320
return success;
339-
}
321+
}
340322

341323

342324
wxBitmap GeoTools::StaticMap(double lat, double lon, int zoom, MapProvider service) {
343-
if (zoom > 21) zoom = 21;
344-
if (zoom < 1) zoom = 1;
325+
326+
rapidjson::GenericDocument < rapidjson::UTF16<> > reader;
327+
wxString str;
328+
329+
// Azure get static map documentation https://learn.microsoft.com/en-us/rest/api/maps/render/get-map-static-image
330+
// valid zoom range is 0-19 for tilesetId = microsoft.imagery
331+
if (zoom > 19) zoom = 21;
332+
if (zoom < 0) zoom = 0;
345333
wxString zoomStr = wxString::Format("%d", zoom);
346334

347335
wxString url;
@@ -353,13 +341,46 @@ wxBitmap GeoTools::StaticMap(double lat, double lon, int zoom, MapProvider servi
353341

354342
}
355343
else {
356-
url = SamApp::WebApi("bing_maps_imagery_api");
344+
url = SamApp::WebApi("azure_maps_static_map_api");
357345
url.Replace("<POINT>", wxString::Format("%.14lf,%.14lf", lat, lon));
358346
url.Replace("<ZOOMLEVEL>", zoomStr);
359-
url.Replace("<BINGAPIKEY>", wxString(bing_api_key));
347+
url.Replace("<LONLAT>", wxString::Format("%.14lf,%.14lf", lon, lat));
348+
url.Replace("<AZUREAPIKEY>", wxString(azure_api_key));
360349
}
361350

362351
wxEasyCurl curl;
352+
353+
curl.AddHttpHeader("Accept: image/png");
354+
363355
bool ok = curl.Get(url, "Obtaining aerial imagery...");
364-
return ok ? wxBitmap(curl.GetDataAsImage(wxBITMAP_TYPE_JPEG)) : wxNullBitmap;
356+
357+
str = curl.GetDataAsString();
358+
reader.Parse(str.c_str());
359+
360+
// curl Get failed
361+
if (!ok) {
362+
wxMessageBox("Static Map Error!\nFailed to download static map.");
363+
return wxNullBitmap;
364+
}
365+
366+
str = curl.GetDataAsString();
367+
// returned JSON string instead of image, probably an error message
368+
if (str != "") {
369+
reader.Parse(str.c_str());
370+
if (reader.HasMember(L"error")) {
371+
if (reader[L"error"].HasMember(L"message")) {
372+
if (reader[L"error"][L"message"].IsString()) {
373+
wxMessageBox(wxString::Format("Static Map Error!\n%s", reader[L"error"][L"message"].GetString()));
374+
return wxNullBitmap;
375+
}
376+
}
377+
wxMessageBox(wxString::Format("Static Map Error!\n%s", reader[L"error"][L"code"].GetString()));
378+
return wxNullBitmap;
379+
}
380+
wxMessageBox(wxString::Format("Static Map Error!\nNo map image."));
381+
return wxNullBitmap;
382+
}
383+
else {
384+
return ok ? wxBitmap(curl.GetDataAsImage(wxBITMAP_TYPE_PNG)) : wxNullBitmap;
385+
}
365386
}

src/geotools.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ class GeoTools
5151
double* lat, double* lon, double* tz = 0, bool showprogress = true);
5252

5353
enum MapProvider {
54-
GOOGLE_MAPS, BING_MAPS
54+
GOOGLE_MAPS, AZURE_MAPS
5555
};
5656

5757
// Return a map for a given lat/lon and zoom level as a bitmap image

src/invoke.cpp

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3794,19 +3794,34 @@ static bool copy_mat(lk::invoke_t &cxt, wxString sched_name, matrix_t<double> &m
37943794
return true;
37953795
}
37963796

3797-
void fcall_geocode(lk::invoke_t& cxt)
3797+
void fcall_geocode(lk::invoke_t& cxt)
37983798
{
37993799
LK_DOC("geocode",
3800-
"Given a street address or location name, returns latitude, longitude, and time zone. Not designed to take latitude and longitude as input. Uses the MapQuest Geocoding API via a private NREL wrapper. Returned table fields are 'lat', 'lon', 'tz', 'ok'.",
3801-
"(string):table");
3800+
"Given a street address or location name, returns latitude, longitude. Returns optional time zone if get_tz is true. Not designed to take latitude and longitude as input. Uses the MapQuest Geocoding API via a private NREL wrapper. Returned table fields are 'lat', 'lon', 'tz', 'ok'.",
3801+
"(string:location, [boolean:get_tz]):table");
38023802

3803-
double lat = 0, lon = 0, tz = 0;
3804-
// use GeoTools::GeocodeGoogle for non-NREL builds and set google_api_key in private.h
3805-
bool ok = GeoTools::GeocodeDeveloper(cxt.arg(0).as_string(), &lat, &lon, &tz);
3803+
bool get_tz = false;
3804+
if (cxt.arg_count() > 1) {
3805+
get_tz = cxt.arg(1).as_boolean();
3806+
}
3807+
3808+
double lat = std::numeric_limits<double>::quiet_NaN();
3809+
double lon = std::numeric_limits<double>::quiet_NaN();
3810+
double tz = std::numeric_limits<double>::quiet_NaN();
3811+
bool ok = false;
38063812
cxt.result().empty_hash();
3813+
3814+
// use GeoTools::GeocodeGoogle for non-NREL builds and set google_api_key in private.h
3815+
if (get_tz) {
3816+
ok = GeoTools::GeocodeDeveloper(cxt.arg(0).as_string(), &lat, &lon, &tz);
3817+
cxt.result().hash_item("tz").assign(tz);
3818+
}
3819+
else {
3820+
ok = GeoTools::GeocodeDeveloper(cxt.arg(0).as_string(), &lat, &lon);
3821+
}
3822+
38073823
cxt.result().hash_item("lat").assign(lat);
38083824
cxt.result().hash_item("lon").assign(lon);
3809-
cxt.result().hash_item("tz").assign(tz);
38103825
cxt.result().hash_item("ok").assign(ok ? 1.0 : 0.0);
38113826
}
38123827

src/main.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ struct smart_ptr
6262
extern const char *sam_api_key;
6363
extern const char* geocode_api_key;
6464
extern const char* google_api_key;
65-
extern const char* bing_api_key;
65+
extern const char* azure_api_key;
6666

6767
class wxSimplebook;
6868
class wxPanel;

src/main_add.h

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -375,10 +375,9 @@ extern void RegisterReportObjectTypes();
375375

376376

377377
wxEasyCurl::Initialize();
378-
//wxEasyCurl::SetApiKeys( GOOGLE_API_KEY, BING_API_KEY, DEVELOPER_API_KEY );
379378
wxEasyCurl::SetUrlEscape("<SAMAPIKEY>", wxString(sam_api_key));
380379
wxEasyCurl::SetUrlEscape("<GEOCODEAPIKEY>", wxString(geocode_api_key));
381-
wxEasyCurl::SetUrlEscape("<BINGAPIKEY>", wxString(bing_api_key));
380+
wxEasyCurl::SetUrlEscape("<AZUREAPIKEY>", wxString(azure_api_key));
382381
wxEasyCurl::SetUrlEscape("<GOOGLEAPIKEY>", wxString(google_api_key));
383382

384383
wxEasyCurl::SetUrlEscape("<USEREMAIL>", wxString(user_email));

src/private.h

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,11 @@ const char* user_email = "";
4949
// Requires Google Cloud account and subscription https://cloud.google.com
5050
const char *google_api_key = "";
5151

52-
// Bing Map APIs:
52+
// Azure Map APIs:
5353
// Used for static map underlay in 3D shade calculator (Google map can be used as an option instead)
54-
// Get a Bing Maps developer key at https://www.bingmapsportal.com/
55-
const char *bing_api_key = "";
54+
// Used for time zone API for geocoding
55+
// Get a an Azure Maps key at https://azure.microsoft.com/
56+
const char *azure_api_key = "";
5657

5758
// Private NREL Developer geocoding API for NREL versions of SAM
5859
const char *geocode_api_key = "";

0 commit comments

Comments
 (0)