Mapbox GL: How can I handle invalid/expired access tokens? - mapbox

I've implemented Mapbox GL:
script.src = 'https://api.mapbox.com/mapbox-gl-js/v2.8.2/mapbox-gl.js';
script.onload = function() {
mapboxgl.accessToken = 'invalid_token';
map = new mapboxgl.Map({
container: 'mapsection', // container ID
style: 'mapbox://styles/mapbox/streets-v11' // style URL
});
}
If the access token is invalid or expired then a message is shown in the console, but how can I handle this in my code? I've tried both try .. catch and map.on('error'), but neither acknowledge there is an error. Any operations on the map are performed without errors, but there is just nothing to see on the page.
Alternatively, is there an API to validate a given token?

I don't know for sure, but if you take one of the URLs that are being requested (by looking in developer tools), and using fetch to query that URL, you will probably get back either 200 for a correct token, or 401 or 403 for an invalid token (or other issue).

Looks like I was almost there, but just made a small mistake. It is indeed the map.on('error') event handler I need to use:
script.src = 'https://api.mapbox.com/mapbox-gl-js/v2.8.2/mapbox-gl.js';
script.onload = function() {
mapboxgl.accessToken = 'invalid_token';
map = new mapboxgl.Map({
container: 'mapsection', // container ID
style: 'mapbox://styles/mapbox/streets-v11' // style URL
});
map.on('error', (response) => {
alert(response.error.message)
});
}

Using map.on('error') results in Mapbox GL (v2.12.0) creating the full HTML DIV structure even when a Mapbox access token is invalid.
<div id="map-container" class="mapboxgl-map"><div class="mapboxgl-canary" style="visibility: hidden;"></div><div class="mapboxgl-canvas-container mapboxgl-interactive mapboxgl-touch-drag-pan mapboxgl-touch-zoom-rotate"><canvas class="mapboxgl-canvas" tabindex="0" aria-label="Map" role="region" width="780" height="724" style="width: 519.115px; height: 482.542px;"></canvas></div><div class="mapboxgl-control-container"><div class="mapboxgl-ctrl-top-left"></div><div class="mapboxgl-ctrl-top-right"><div class="mapboxgl-ctrl mapboxgl-ctrl-group"><button class="mapboxgl-ctrl-zoom-in" type="button" aria-label="Zoom in" aria-disabled="false"><span class="mapboxgl-ctrl-icon" aria-hidden="true" title="Zoom in"></span></button><button class="mapboxgl-ctrl-zoom-out" type="button" aria-label="Zoom out" aria-disabled="false"><span class="mapboxgl-ctrl-icon" aria-hidden="true" title="Zoom out"></span></button><button class="mapboxgl-ctrl-compass" type="button" aria-label="Reset bearing to north"><span class="mapboxgl-ctrl-icon" aria-hidden="true" title="Reset bearing to north" style="transform: rotate(0deg);"></span></button></div></div><div class="mapboxgl-ctrl-bottom-left"><div class="mapboxgl-ctrl" style="display: block;"><a class="mapboxgl-ctrl-logo" target="_blank" rel="noopener nofollow" href="https://www.mapbox.com/" aria-label="Mapbox logo"></a></div></div><div class="mapboxgl-ctrl-bottom-right"></div></div></div>
To avoid the unnecessary code execution by mapbox-gl.js, I used #Steve's suggestion of using a fetch query to a Mapbox API. A request to a map styles API URL results in an ~70KB response when the access token is valid. A request to the Mapbox geocoding API (version 5 is older; v6 is the most current version as of Feb 2023), using a non-existent place as the search string results in a 343 byte response.
const url = `https://api.mapbox.com/geocoding/v5/mapbox.places/rndstrasdf.json?access_token=${mapboxAccessToken}`;
This all seems unnecessary, however, as it would more efficient if Mapbox provided an access token verification API before executing any mapbox-gl in much the same way they provide a mapbox-gl-supported plugin.
For performance reasons, it is better to check that Mapbox GL JS is
supported before going to the trouble of loading the script and
initializing the map on your page.
document.addEventListener('DOMContentLoaded', function() {
loadMap()
.then(map => console.log("Map loaded successfully into element with ID: " + map._container.id))
.catch(error => console.error("Map load failed with the error: " + error.message));
});
function loadMap() {
return new Promise((resolve, reject) => {
const mapboxAccessToken = "ADD_YOUR_VALID_OR_INVALID_ACCESS_TOKEN";
// Using the following URL in a 'fetch' API results in a ~70KB response.
//const url = `https://api.mapbox.com/styles/v1/mapbox/streets-v11?access_token=${mapboxAccessToken}`;
//const url = `https://api.mapbox.com/styles/v1/mapbox/streets-v11?access_token=invalid`;
// A URL to Mapbox geocoding to validate a Mapbox access token
// results in a 343 byte response using a non-existent place name.
// Place search at https://www.mapbox.com/geocoding
// Temporary Geocoding API pricing https://www.mapbox.com/pricing#search
// A valid place name -> "Los Angeles"
//const url = `https://api.mapbox.com/geocoding/v5/mapbox.places/Los Angeles.json?access_token=${mapboxAccessToken}`;
const url = `https://api.mapbox.com/geocoding/v5/mapbox.places/rndstrasdf.json?access_token=${mapboxAccessToken}`;
fetch(url)
.then(response => {
if (!response.ok) {
response.message = "Connected to Mapbox service but with an invalid access token.";
reject(response);
return;
}
// Request succeeded. Response is an empty GeoJSON 'features' collection
// 343 bytes
/*
'{"type":"FeatureCollection","query":["rndstrasdf"],"features":[],
"attribution":"NOTICE: © 2023 Mapbox and its suppliers. All rights reserved.
Use of this data is subject to the Mapbox Terms of Service
(https://www.mapbox.com/about/maps/). This response and the information
it contains may not be retained. POI(s) provided by Foursquare."}'
*/
response.text().then(text => {
console.log(text);
});
mapboxgl.accessToken = mapboxAccessToken;
// stackoverflow.com/questions/72254578/how-to-solve-that-a-valid-mapbox-access-token-is-required-to-use-mapbox-gl-js
// github.com/mapbox/mapbox-gl-js/releases/tag/v2.0.0
// "Beginning with v2.0.0, a billable map load occurs whenever a
// Map object is initialized. Before updating an existing
// implementation from v1.x.x to v2.x.x, please review the
// pricing documentation to estimate expected costs."
const map = new mapboxgl.Map({
container: "map-container",
style: 'mapbox://styles/mapbox/streets-v11',
center: [12.79690, 47.32350], // Longitude, latitude
zoom: 5
});
// Add zoom and rotation controls to the map
// docs.mapbox.com/mapbox-gl-js/example/navigation
map.addControl(new mapboxgl.NavigationControl());
map.on('load', () => resolve(map));
map.on('error', error => reject(error));
})
.catch(error => {
reject(error);
});
});
}
<link href='https://api.mapbox.com/mapbox-gl-js/v2.12.0/mapbox-gl.css' rel='stylesheet' />
<script src='https://api.mapbox.com/mapbox-gl-js/v2.12.0/mapbox-gl.js'></script>
<div id="map-container" style="width: 100%; height: 80vh;"></div>

Related

How to use Mapbox geocoder with Quasar select?

I'm trying to create an Autocomplete component using the Mapbox Geocode API and Quasar's <q-select /> component. It appears though that Mapbox requires using their input (could be wrong about this), so I'm having trouble hooking it up to the select.
I've tried using the #mapbox/mapbox-gl-geocoder, vue-mapbox-ts and v-mapbox-geocoder libraries now. The two third-party libraries had some issues with them, so I'd prefer to use the one direct from Mapbox if possible.
<template>
<q-select
v-model="state.location"
:options="state.locations?.features"
:option-value="(result: MapboxGeocoder.Result) => result.place_name"
:option_label="(result: MapboxGeocoder.Result) => result.place_name"
:loading="state.loadingResults"
clear-icon="clear"
dropdown-icon="expand_more"
clearable
outlined
use-input
dense
label="Location">
<template #prepend>
<q-icon name="place " />
</template>
</q-select>
</template>
<script lang='ts' setup>
import { reactive, ref, onMounted } from 'vue';
import MapboxGeocoder from '#mapbox/mapbox-gl-geocoder';
const accessToken = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN as string;
const state = reactive({
first_name: auth.currentUser?.first_name || undefined,
last_name: auth.currentUser?.last_name || undefined,
location: undefined,
locations: undefined as undefined | MapboxGeocoder.Results,
loadingResults: false,
geocoder: null as null | MapboxGeocoder,
});
onMounted(() => {
state.geocoder = new MapboxGeocoder({
accessToken,
types: 'country,region,place,postcode,locality,neighborhood',
});
state.geocoder?.on('result', (e) => {
console.log('on result: ', e);
});
state.geocoder?.on('results', (e) => {
console.log('results: ', e);
state.locations = e.features;
});
state.geocoder?.on('loading', (e) => {
console.log('loading');
state.loadingResults = true;
});
});
</script>
In the code sample above, none of the console logs are being run. If I add an empty <div id="geocoder" /> and then use the state.geocoder.addTo('#geocoder') function, it renders the Mapbox input and hits the console logs, but then I am unable to use the Quasar select like I'm hoping to.
How can I go about accomplishing this?
I never tracked down the reason why your seemingly correct syntax failed, but if I used this alternative:
const function results(e) {
console.log('results: ', e);
state.locations = e.features;
}
state.geocoder?.on('results', results);
everything magically worked.
MapboxGeocoder is a UI control, it's not meant to be used in a "headless" mode.
As you create your own control, you could just use the Mapbox Geocoder API, see https://docs.mapbox.com/api/search/geocoding/ for more information on how this works.

How to catch error at Mapbox tile request for invalid or expired token

We are using mapbox-gl library for to load tiles like bing tile and our internal tiles.
this.map = new mapboxgl.Map({
container: options.mapContainer,
style: this.getBaseMapStyle(),
minZoom: options.minZoom,
maxZoom: options.maxZoom,
TransformType: ImageryWarehouseTransform,
});
We are passing tileUrl to as per our need. But question is how to catch error if access token is invalid or expired.
I checked this but nothing is coming here. Is there something wrong I am doing.
map.on('error', () => {
console.log('A error event occurred.');
});
The following code works for me, catching invalid tokens:
mapboxgl.accessToken = 'invalid_token';
map = new mapboxgl.Map({
container: 'mapsection', // container ID
style: 'mapbox://styles/mapbox/streets-v11' // style URL
});
map.on('error', (response) => {
alert(response.error.message)
});

How to manage contacts using Contacts API in our website correctly?

I was trying to integrate Google Contacts API to manage the contacts in my website.
I've done the following things:
I've created an application in google developer console and added http://localhost:4200 as URIs & Authorized redirect URIs.
Enabled 'Contacts API'.
I've added the following in my index.html (I've replaced {clientID} with my original client ID (of course):
<script>
function loadAuthClient() {
gapi.load('auth2', function() {
gapi.auth2.init({
client_id: '{clientID}'
}).then(() => {
console.log("success");
}).catch(err => {
console.log(err);
});
});
}
</script>
<script src="https://apis.google.com/js/platform.js?onload=loadAuthClient" async defer></script>
<meta name="google-signin-client_id" content="{clientID}">
Signed in successfully using:
gapi.auth2.getAuthInstance().signIn().then(() => {
console.log("Logged in")
}).catch(err => {
console.log(err);
});
Tried fetching the contacts using the following:
var user = gapi.auth2.getAuthInstance().currentUser.get();
var idToken = user.getAuthResponse().id_token;
var endpoint = `https://www.google.com/m8/feeds/contacts/`;
var xhr = new XMLHttpRequest();
xhr.open('GET', endpoint + '?access_token=' + encodeURIComponent(idToken));
xhr.setRequestHeader("Gdata-Version", "3.0");
xhr.onreadystatechange = function () {
if (this.readyState === XMLHttpRequest.DONE && this.status === 200) {
window.alert(xhr.responseText);
}
};
xhr.send();
But I'm getting the error:
Access to XMLHttpRequest at 'https://www.google.com/m8/feeds/contacts/?access_token={I removed the access token}' from origin 'http://localhost:4200' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Can someone please guide me where I'm going wrong?
My original response was off the mark. The actual answer is much simpler.
In step 4, try changing your endpoint:
var endpoint = `https://www.google.com/m8/feeds/contacts/default/full`;
In my local tests, this resulted in the expected response.
Another suggestion is to add alt=json to your query, so that you get easy to parse JSON payload. Otherwise you'll get a nasty XML payload in the response.
Here's the updated step 4 with these changes:
var endpoint = `https://www.google.com/m8/feeds/contacts/default/full`;
var xhr = new XMLHttpRequest();
xhr.open('GET', endpoint + '?access_token=' + encodeURIComponent(idToken) + '&alt=json');
xhr.setRequestHeader("Gdata-Version", "3.0");
xhr.onreadystatechange = function () {
if (this.readyState === XMLHttpRequest.DONE && this.status === 200) {
window.alert(xhr.responseText);
}
};
xhr.send();
Here's my original response, just in case it helps someone else.
I suspect that you'll need to add http://localhost:4200 to your list of "Authorized JavaScript origins" for the OAuth Client that you are using.
Edit your OAuth 2.0 Client ID and add the URI to the Javascript origins as below:
The other section on that page, Authorized Redirect URIs, only permits the OAuth flow to be redirected back to your web app. Often your web app server will actually consume the APIs so Google doesn't automatically permit CORS access to these APIs to keep things secure.

Using GitHub list-issues-for-a-repository API

When you go to GitHub, under Issues, it pulls up all the open issues as an HTML page. We'd like to implement a dashboard showing all the issues in a repository, grouped by labels, including those issues which are not correctly labelled.
This is the corresponding list-issues-for-a-repository API.
While I was initially using jQuery and Javascript, am now using PHP for a proof-of-concept because its built-in session handling lets me use the same page to login, have GitHub authenticate & callback, and continue. But it doesn't matter to me, any language is okay.
I've managed to get access to the GitHub API via OAUTH2, but when I get the list of repositories via https://api.github.com/orgs/{org}/repos it comes up as an empty array.
Because the /orgs/{org}/repos API returns an empty array, of course the corresponding /repos/{org}/{repo}/issues API will return an error.
Edit: See this followup for a solution! Glad I finally got it working!
It is a rest API. You need to call some endpoints using an Http request.
I don't know what language you are trying to use so I can't give you a good example on how to acheive this.
If you don't know which language to use yet, use postman to create REST API call to the github API.
Let's say you want to retreive the issues of the microsoft's typescript repo, You would need to call this API endpoint :
https://api.github.com/repos/microsoft/typescript/issues
Notice here that i have replace the :owner and :repo value of documentation for the one i'm trying to get.
You can then pass some parameters to the call to filter your data, for example, the API label.
https://api.github.com/repos/microsoft/typescript/issues?labels=API
This will only return issues that are labelled as API.
This is the basics of how to use an API.
You can use jQuery Ajax to access the Github API and add a basic authentication header to authenticate (see here), an example is shown below, this will pull the issues for a given repo and show the first 10 in an alert window.
See the documentation on pulling issues here: https://developer.github.com/v3/issues/ to see which parameters you can use to filter, sort etc.
For example you can get all issues labelled 'bug' using:
/issues?labels=bug
This can include multiple labels, e.g.
/issues?labels=enhancement,nicetohave
You could easily modify to list in a table etc.
const username = 'github_username'; // Set your username here
const password = 'github_password'; // Set your password here
const repoPath = "organization/repo" // Set your Repo path e.g. microsoft/typescript here
$(document).ready(function() {
$.ajax({
url: `https://api.github.com/repos/${repoPath}/issues`,
type: "GET",
crossDomain: true,
// Send basic authentication header.
beforeSend: function (xhr) {
xhr.setRequestHeader ("Authorization", "Basic " + btoa(username + ":" + password));
},
success: function (response) {
console.log("Response:", response);
alert(`${repoPath} issue list (first 10):\n - ` + response.slice(0,10).map(issue => issue.title).join("\n - "))
},
error: function (xhr, status) {
alert("error: " + JSON.stringify(xhr));
}
});
});
Below is a snippet listing issues for a (public) repo using jQuery and the Github API:
(Note we don't add an authentication header here!)
const repoPath = "leachim6/hello-world" //
$(document).ready(function() {
$.ajax({
url: `https://api.github.com/repos/${repoPath}/issues`,
type: "GET",
crossDomain: true,
success: function (response) {
tbody = "";
response.forEach(issue => {
tbody += `<tr><td>${issue.number}</td><td>${issue.title}</td><td>${issue.created_at}</td><td>${issue.state}</td></tr>`;
});
$('#output-element').html(tbody);
},
error: function (xhr, status) {
alert("error: " + JSON.stringify(xhr));
}
});
});
<head>
<meta charset="utf-8">
<title>Issue Example</title>
<link rel="stylesheet" href="css/styles.css?v=1.0">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.4.1.min.js" crossorigin="anonymous"></script>
</head>
<body style="margin:50px;padding:25px">
<h3>Issues in Repo</h3>
<table class="table table-striped">
<thead>
<tr>
<th scope="col">Issue #</th>
<th scope="col">Title</th>
<th scope="col">Created</th>
<th scope="col">State</th>
</tr>
</thead>
<tbody id="output-element">
</tbody>
</table>
</body>

Twitter Typeahead.js with Yahoo Finance in AJAX

I am trying to couple the new version of Typeahead.js and using it with JSON that needs to be pulled from AJAX and not from a JSON file like they have in their examples. I just can't get it to work, I don't want to cache the JSON result or anything, I want to pull it live from Yahoo.
My HTML input is <input type="text" id="symbol" name="symbol" autofocus autocomplete="off" placeholder="Symbol" onkeyup="onSymbolChange(this.value)" />
My AJAX/PHP file has this to retrieve data (this part work, I tested it with Firebug)
header('Content-type:text/html; charset=UTF-8;');
$action = (isset($_GET['action'])) ? $_GET['action'] : null;
$symbol = (isset($_GET['symbol'])) ? $_GET['symbol'] : null;
switch($action) {
case 'autocjson':
getYahooSymbolAutoComplete($symbol);
break;
}
function getYahooSymbolAutoCompleteJson($symbolChar) {
$data = #file_get_contents("http://d.yimg.com/aq/autoc?callback=YAHOO.util.ScriptNodeDataSource.callbacks&query=$symbolChar");
// parse yahoo data into a list of symbols
$result = [];
$json = json_decode(substr($data, strlen('YAHOO.util.ScriptNodeDataSource.callbacks('), -1));
foreach ($json->ResultSet->Result as $stock) {
$result[] = '('.$stock->symbol.') '.$stock->name;
}
echo json_encode(['symbols' => $result]);
}
The JS file (this is where I'm struggling)
function onSymbolChange(symbolChar) {
$.ajax({
url: 'yahoo_autocomplete_ajax.php',
type: 'GET',
dataType: 'json',
data: {
action: 'autocjson',
symbol: symbolChar
},
success: function(response) {
$('#symbol').typeahead({
name: 'symbol',
remote: response.symbols
});
}
});
}
I don't think that I'm suppose to attach a typeahead inside an AJAX success response, but I don't see much examples with AJAX (except for a previous version of typeahead)... I see the JSON response with Firebug after typing a character but the input doesn't react so good. Any guidance would really be appreciated, I'm working on a proof of concept at this point... It's also worth to know that I'm using AJAX because I am in HTTPS and using a direct http to Yahoo API is giving all kind of problems with Chrome and new Firefox for insecure page.
UPDATE
To make it to work, thanks to Hieu Nguyen, I had to modify the AJAX JSON response from this echo json_encode(['symbols' => $result]); to instead this echo json_encode($result); and modify the JS file to use the code as suggested here:
$('#symbol').typeahead({
name: 'symbol',
remote: 'yahoo_autocomplete_ajax.php?action=autocjson&symbol=%QUERY'
});
I have to do it in reverse, i.e: hook the ajax call inside typeahead remote handler. You can try:
$('#symbol').typeahead({
name: 'symbol',
remote: '/yahoo_autocomplete_ajax.php?action=autocjson&symbol=%QUERY'
});
You don't have to create onSymbolChange() function since typeahead will take care of that already.
You can also filter and debug the response from backend by using:
$('#symbol').typeahead({
name: 'symbol',
remote: {
url: '/yahoo_autocomplete_ajax.php?action=autocjson&symbol=%QUERY',
filter: function(resp) {
var dataset = [];
console.log(resp); // debug the response here
// do some filtering if needed with the response
return dataset;
}
}
});
Hope it helps!