CTA Bus Map
This intermediate guide walks you through showing Chicago Transit Authority (CTA) bus locations on a Google Map. You'll learn how to create a StacheElement that integrates with 3rd party widgets.
In this guide, you will learn how to:
- Use
fetch
to request data. - Create a custom element that wraps a google map.
- Add markers to the google map.
The final widget looks like:
See the Pen CTA Bus Map (Medium) by Bitovi (@bitovi) on CodePen.
To use the widget:
- Click a Bus Route.
- Explore the markers added to the Google Map showing the bus locations for that route.
- Click the route name overlay to refresh the bus locations.
The following sections are broken down into the following parts:
- The problem — A description of what the section is trying to accomplish.
- What you need to know — Information about CanJS that is useful for solving the problem.
- How to verify it works - How to make sure the solution works if it’s not obvious.
- The solution — The solution to the problem.
Setup
START THIS TUTORIAL BY CLICKING THE “EDIT ON CODEPEN” BUTTON IN THE TOP RIGHT CORNER OF THE FOLLOWING EMBED::
See the Pen CTA Bus Map (Medium) [Starter] by Bitovi (@bitovi) on CodePen.
This CodePen has initial prototype HTML and CSS which is useful for getting the application to look right.
What you need to know
There's nothing to do in this step. The CodePen is already setup with:
- A basic CanJS setup.
- A promise that resolves when the Google Maps has loaded.
- Some variables useful to make requests to get bus routes and locations.
Please read on to understand the setup.
A Basic CanJS Setup
A basic CanJS setup uses properties within a StacheElement to manage the behavior of a View as follows:
import { StacheElement } from "can"; // Define the Component class MyComponent extends StacheElement { // The component View static view = ``; // The component properties static props = {}; } // The component custom tag customElements.define("my-component", MyComponent);
The component can be rendered by adding the custom tag to the HTML:
<my-component></my-component>
CanJS StacheElement uses can-stache to render data in a template and keep it live. Templates can be authored in the component's
view
property like:import { StacheElement } from "can"; // Define the Component class MyComponent extends StacheElement { static view = `TEMPLATE CONTENT`; } customElements.define("my-component", MyComponent);
A can-stache template uses {{key}} "magic tags" to insert data into the HTML output like:
import { StacheElement } from "can"; // Define the Component class MyComponent extends StacheElement { static view = `{{something.name}}`; } customElements.define("my-component", MyComponent);
Mount the component in the HTML:
<my-component></my-component>
Loading Google Maps API
The following loads Google Maps API:
<bus-tracker></bus-tracker>
<script>
window.googleAPI = new Promise(function(resolve) {
const script = document.createElement("script");
script.src = "https://maps.googleapis.com/maps/api/js?key=AIzaSyD7POAQA-i16Vws48h4yRFVGBZzIExOAJI";
document.body.appendChild(script);
script.onload = resolve;
});
</script>
It creates a global googleAPI
promise that resolves when Google Maps is ready. You can use it like:
googleAPI.then(function() {
new google.maps.Map( /* ... */ );
})
Loading CTA Bus Data
This app needs to make requests to the http://www.ctabustracker.com/ API. The
ctabustracker
API is hosted at:
const apiRoot = "http://www.ctabustracker.com/bustime/api/v2/"
The API needs a token as part of the request:
const token = "?key=piRYHjJ5D2Am39C9MxduHgRZc&format=json";
However, the API does not support cross origin requests. Therefore, we will request data using a proxy hosted at:
const proxyUrl = "https://can-cors.herokuapp.com/"
With that proxy, the requests for this app will look like:
fetch("https://can-cors.herokuapp.com/"+
"http://www.ctabustracker.com/bustime/api/v2/"+
"getroutes"+
"?key=piRYHjJ5D2Am39C9MxduHgRZc&format=json")
Change the app title
The problem
In this section, we will:
- Explore the relationship between component Props and View.
- Make it so the title of the page changes from
<h1>YOUR TITLE HERE</h1>
to<h1>CHICAGO CTA BUS TRACKER</h1>
. - Let us adjust the title simply by changing the property like:
element.title = "TITLE UPDATED"
What you need to know
A can-stache template uses {{key}} magic tags to insert data into the HTML output like:
import { StacheElement } from "can"; class MyComponent extends StacheElement { static view = `{{this.someValue}}`; } customElements.define("my-component", MyComponent);
These values come from the component properties.
The StacheElement props allows you to define a property with a default value like:
import { StacheElement } from "can"; class MyComponent extends StacheElement { static view = `{{this.someValue}}`; static props = { someValue: { default: "This string" } } } customElements.define("my-component", MyComponent);
How to verify it works
Run the following in the Console
tab:
document.querySelector('my-component').someValue = "TITLE UPDATED";
You should see the title update.
The solution
Update the JavaScript tab to:
import { StacheElement } from "//unpkg.com/can@6/core.mjs";
const proxyUrl = "https://can-cors.herokuapp.com/";
const token = "?key=piRYHjJ5D2Am39C9MxduHgRZc&format=json";
const apiRoot = "http://www.ctabustracker.com/bustime/api/v2/";
const getRoutesEnpoint = apiRoot + "getroutes" + token;
const getVehiclesEndpoint = apiRoot + "getvehicles" + token;
class BusTracker extends StacheElement {
static view = `
<div class="top">
<div class="header">
<h1>{{ this.title }}</h1>
<p>Loading routes…</p>
</div>
<ul class="routes-list">
<li>
<span class="route-number">1</span>
<span class="route-name">Bronzeville/Union Station</span>
<span class="check">✔</span>
</li>
<li class="active">
<span class="route-number">2</span>
<span class="route-name">Hyde Park Express</span>
<span class="check">✔</span>
</li>
</ul>
</div>
<div class="bottom">
<div class="route-selected">
<small>Route 2:</small> Hyde Park Express
<div class="error-message">No vehicles available for this route</div>
</div>
<div class="gmap">Google map will go here.</div>
</div>
`;
static props = {
title: {
default: "Chicago CTA Bus Tracker"
}
};
}
customElements.define("bus-tracker", BusTracker);
List bus routes
The problem
In this section, we will:
- Load and list bus routes.
- Show
<p>Loading routes…</p>
while loading routes.
We will do this by:
- Store the promise of bus routes in a
routesPromise
property. routesPromise
will resolve to anArray
of the routes.- Loop through each route and add an
<li>
to the page. - Show the loading message while
routesPromise
is pending.
What you need to know
The default property definition can return the initial value of a property like:
import { ObservableObject } from "can" class AppViewModel extends ObservableObject { static props = { myProperty: { get default() { return new Promise( /* ... */ ); } } }; } new AppViewModel().myProperty //-> Promise
The fetch API is an easy way to make requests to a URL and get back JSON. Use it like:
fetch(url).then(function(response) { return response.json(); }).then(function(data) { });
You'll want to use the
proxyUrl
andgetRoutesEnpoint
variables to make a request for CTA bus routes. The routes service returns data like:{ "bustime-response": { "routes": [ { "rt": "1", "rtnm": "Bronzeville/Union Station", "rtclr": "#336633", "rtdd": "1" }, // ... ] } }
Make sure that
routesPromise
will be a Promise that resolves to an array of routes.Promises can transform data by returning new values. For example if
outerPromise
resolves to{innerData: {name: "inner"}}
,resultPromise
will resolve to{name: "inner"}
:const resultPromise = outerPromise.then(function(data) { return data.innerData; });
Use {{# if(value) }} to do
if/else
branching in can-stache.Use {{# for(of) }} to do looping in can-stache.
Promises are observable in can-stache. Given a promise
somePromise
, you can:- Check if the promise is loading like:
{{# if(somePromise.isPending) }}
. - Loop through the resolved value of the promise like:
{{# for(item of somePromise.value) }}
.
- Check if the promise is loading like:
The solution
Update the JavaScript tab to:
import { StacheElement } from "//unpkg.com/can@6/core.mjs";
const proxyUrl = "https://can-cors.herokuapp.com/";
const token = "?key=piRYHjJ5D2Am39C9MxduHgRZc&format=json";
const apiRoot = "http://www.ctabustracker.com/bustime/api/v2/";
const getRoutesEnpoint = apiRoot + "getroutes" + token;
const getVehiclesEndpoint = apiRoot + "getvehicles" + token;
class BusTracker extends StacheElement {
static view = `
<div class="top">
<div class="header">
<h1>{{ this.title }}</h1>
{{# if(this.routesPromise.isPending) }}<p>Loading routes…</p>{{/ if }}
</div>
<ul class="routes-list">
{{# for(route of this.routesPromise.value) }}
<li>
<span class="route-number">{{ route.rt }}</span>
<span class="route-name">{{ route.rtnm }}</span>
<span class="check">✔</span>
</li>
{{/ for }}
</ul>
</div>
<div class="bottom">
<div class="route-selected">
<small>Route 2:</small> Hyde Park Express
<div class="error-message">No vehicles available for this route</div>
</div>
<div class="gmap">Google map will go here.</div>
</div>
`;
static props = {
title: {
default: "Chicago CTA Bus Tracker"
},
routesPromise: {
get default() {
return fetch(proxyUrl + getRoutesEnpoint)
.then(response => response.json())
.then(data => data["bustime-response"].routes);
}
}
};
}
customElements.define("bus-tracker", BusTracker);
Pick a route and log bus locations
The problem
In this section, we will:
- Highlight the selected bus route after a user clicks it.
- Log the bus (vehicle) locations for the selected route.
We will do this by:
- Listening to when a user clicks one of the bus routes.
- Adding
active
to the class name of that route's<li>
like:<li class="active">
. - Making the request for the vehicle locations of the selected route.
What you need to know
Use on:event to listen to an event on an element and call a method in can-stache. For example, the following calls
doSomething()
when the<div>
is clicked:<div on:click="doSomething()"> ... </div>
Use the
"any"
type to define a property of indeterminate type:import { ObservableObject, type } from "can"; class AppViewModel extends ObservableObject { static props = { myProperty: type.Any }; } const viewModel = new AppViewModel({}); viewModel.myProperty = ANYTHING;
You'll want to store the selected bus route as
route
.Use
fetch(proxyUrl + getVehiclesEndpoint + "&rt=" + route.rt)
to get the vehicles for a particular route. If there is route data, it comes back like:{ "bustime-response": { "vehicle": [ { "vid": "8026", "tmstmp": "20171004 09:18", "lat": "41.73921241760254", "lon": "-87.66306991577149", "hdg": "359", "pid": 3637, "rt": "9", "des": "74th", "pdist": 6997, "dly": false, "tatripid": "10002232", "tablockid": "X9 -607", "zone": "" }, // ... ] } }
If there is an error or no buses, the response looks like:
{ "bustime-response": { "error": [ { "rt": "5", "msg": "No data found for parameter" } ] } }
How to verify it works
In the Console
tab, when you click a bus route (like Cottage Grove
), you should see
an array of bus routes.
The solution
Update the JavaScript tab to:
import { StacheElement, type } from "//unpkg.com/can@6/core.mjs";
const proxyUrl = "https://can-cors.herokuapp.com/";
const token = "?key=piRYHjJ5D2Am39C9MxduHgRZc&format=json";
const apiRoot = "http://www.ctabustracker.com/bustime/api/v2/";
const getRoutesEnpoint = apiRoot + "getroutes" + token;
const getVehiclesEndpoint = apiRoot + "getvehicles" + token;
class BusTracker extends StacheElement {
static view = `
<div class="top">
<div class="header">
<h1>{{this.title}}</h1>
{{# if(this.routesPromise.isPending) }}<p>Loading routes…</p>{{/ if }}
</div>
<ul class="routes-list">
{{# for(route of this.routesPromise.value) }}
<li on:click="this.pickRoute(route)" {{# eq(route, this.route) }}class="active"{{/ eq }}>
<span class="route-number">{{ route.rt }}</span>
<span class="route-name">{{ route.rtnm }}</span>
<span class="check">✔</span>
</li>
{{/ for }}
</ul>
</div>
<div class="bottom">
{{# if(this.route) }}
<div class="route-selected">
<small>Route {{ this.route.rt }}:</small> {{ this.route.rtnm }}
<div class="error-message">No vehicles available for this route</div>
</div>
{{/ if }}
<div class="gmap">Google map will go here.</div>
</div>
`;
static props = {
title: {
default: "Chicago CTA Bus Tracker"
},
routesPromise: {
get default() {
return fetch(proxyUrl + getRoutesEnpoint)
.then(response => response.json())
.then(data => data["bustime-response"].routes);
}
},
route: type.Any
};
pickRoute(route) {
this.route = route;
fetch(proxyUrl + getVehiclesEndpoint + "&rt=" + route.rt)
.then(response => response.json())
.then(data => {
if (data["bustime-response"].error) {
console.log(data["bustime-response"].error);
} else {
console.log(data["bustime-response"].vehicle);
}
});
}
}
customElements.define("bus-tracker", BusTracker);
Show when buses are loading and the number of buses
The problem
In this section, we will:
- Show
<p>Loading vehicles…</p>
while bus data is being loaded. - Show
<div class="error-message">No vehicles available for this route</div>
in the overlay if the request for bus data failed. - Show the number of buses inside the
<div class="gmap">
like:Bus count: 20
.
We will do this by:
- Defining and setting a
vehiclesPromise
property.
What you need to know
- In stache, you can check if a promise was rejected like:
{{# if(somePromise.isRejected) }}<p>...</p>{{/ if }}
- The Promise.reject method returns a rejected promise with the provided
reason
:const rejectedPromise = Promise.reject({message: "something went wrong"});
- Promises can transform data by returning new promises. For example if
outerPromise
resolves to{innerData: {name: "inner"}}
,resultPromise
will be a rejected promise with thereason
as{name: "inner"}
:const resultPromise = outerPromise.then(function(data) { return Promise.reject(data.innerData); }); resultPromise.catch(function(reason) { reason.name //-> "inner" });
The solution
Update the JavaScript tab to:
import { StacheElement, type } from "//unpkg.com/can@6/core.mjs";
const proxyUrl = "https://can-cors.herokuapp.com/";
const token = "?key=piRYHjJ5D2Am39C9MxduHgRZc&format=json";
const apiRoot = "http://www.ctabustracker.com/bustime/api/v2/";
const getRoutesEnpoint = apiRoot + "getroutes" + token;
const getVehiclesEndpoint = apiRoot + "getvehicles" + token;
class BusTracker extends StacheElement {
static view = `
<div class="top">
<div class="header">
<h1>{{this.title}}</h1>
{{# if(this.routesPromise.isPending) }}<p>Loading routes…</p>{{/ if }}
{{# if(this.vehiclesPromise.isPending) }}<p>Loading vehicles…</p>{{/ if }}
</div>
<ul class="routes-list">
{{# for(route of this.routesPromise.value) }}
<li on:click="this.pickRoute(route)" {{# eq(route, this.route) }}class="active"{{/ eq }}>
<span class="route-number">{{ route.rt }}</span>
<span class="route-name">{{ route.rtnm }}</span>
<span class="check">✔</span>
</li>
{{/ for }}
</ul>
</div>
<div class="bottom">
{{# if(this.route) }}
<div class="route-selected">
<small>Route {{ this.route.rt }}:</small> {{ this.route.rtnm }}
{{# if(this.vehiclesPromise.isRejected) }}
<div class="error-message">No vehicles available for this route</div>
{{/ if }}
</div>
{{/ if }}
<div class="gmap">Bus count: {{ this.vehiclesPromise.value.length }}</div>
</div>
`;
static props = {
title: {
default: "Chicago CTA Bus Tracker"
},
routesPromise: {
get default() {
return fetch(proxyUrl + getRoutesEnpoint)
.then(response => response.json())
.then(data => data["bustime-response"].routes);
}
},
route: type.Any,
vehiclesPromise: type.Any
};
pickRoute(route) {
this.route = route;
this.vehiclesPromise = fetch(
proxyUrl + getVehiclesEndpoint + "&rt=" + route.rt
)
.then(response => response.json())
.then(data => {
if (data["bustime-response"].error) {
return Promise.reject(data["bustime-response"].error[0]);
} else {
return data["bustime-response"].vehicle;
}
});
}
}
customElements.define("bus-tracker", BusTracker);
Initialize Google Maps to show Chicago
The problem
In this section, we will:
- Create a custom
<google-map-view/>
element that adds a google map. - The google map should be added to a
<div class="gmap"/>
element. - The google map should be centered on Chicago (latitude:
41.881
, longitude-87.623
).
We will do this by:
- Creating a custom StacheElement that adds the
<div class="gmap"/>
to its HTML. - Listens to when the element is in the page and creates a new google map.
What you need to know
Use StacheElement to create custom elements. Start by extending
StacheElement
and then define the custom element with the constructor:import { StacheElement } from "can"; class GoogleMapView extends StacheElement {}; customElements.define("google-map-view", GoogleMapView);
Next, provide the HTML can-stache template with the content you want to insert within the element.
import { StacheElement } from "can"; class GoogleMapView extends StacheElement { static view = `<div class="gmap"/>`; }; customElements.define("google-map-view", GoogleMapView);
Any values you want the custom element to hold must be defined on props. The following specifies a
map
property that can be any value:import { StacheElement, type } from "can"; class GoogleMapView extends StacheElement { static view = `<div class="gmap"/>`; static props = { map: type.Any }; }; customElements.define("google-map-view", GoogleMapView);
An element reference can be passed to the component like the following:
import { StacheElement } from "can"; class GoogleMapView extends StacheElement { static view = `<div this:to="this.mapElement" class="gmap"/>`; static props = { map: type.Any, mapElement: type.maybeConvert(HTMLElement) }; }
StacheElement's connected hook can be used to know when the component's element is inserted into the document as follows:
import { StacheElement, type } from "can"; class GoogleMapView extends StacheElement { static view = `<div this:to="this.mapElement" class="gmap"/>`; static props = { map: type.Any, mapElement: type.maybeConvert(HTMLElement) }; connected() { this // -> the <google-map-view> element this.mapElement // -> the HTML element that renders the map } }; customElements.define("google-map-view", GoogleMapView);
To create a google map, use new google.map.Map( /* ... */ ) once the
googleAPI
has completed loading:new google.maps.Map(gmapDiv, { zoom: 10, center: { lat: 41.881, lng: -87.623 }; })
The solution
Update the JavaScript tab to:
import { StacheElement, type } from "//unpkg.com/can@6/core.mjs";
const proxyUrl = "https://can-cors.herokuapp.com/";
const token = "?key=piRYHjJ5D2Am39C9MxduHgRZc&format=json";
const apiRoot = "http://www.ctabustracker.com/bustime/api/v2/";
const getRoutesEnpoint = apiRoot + "getroutes" + token;
const getVehiclesEndpoint = apiRoot + "getvehicles" + token;
class GoogleMapView extends StacheElement {
static view = `<div this:to="this.mapElement" class="gmap"></div>`;
static props = {
map: type.Any,
mapElement: type.maybeConvert(HTMLElement)
};
connected() {
googleAPI.then(() => {
this.map = new google.maps.Map(this.mapElement, {
zoom: 10,
center: {
lat: 41.881,
lng: -87.623
}
});
});
}
}
customElements.define("google-map-view", GoogleMapView);
class BusTracker extends StacheElement {
static view = `
<div class="top">
<div class="header">
<h1>{{this.title}}</h1>
{{# if(this.routesPromise.isPending) }}<p>Loading routes…</p>{{/ if }}
{{# if(this.vehiclesPromise.isPending) }}<p>Loading vehicles…</p>{{/ if }}
</div>
<ul class="routes-list">
{{# for(route of this.routesPromise.value) }}
<li on:click="this.pickRoute(route)" {{# eq(route, this.route) }}class="active"{{/ eq }}>
<span class="route-number">{{ route.rt }}</span>
<span class="route-name">{{ route.rtnm }}</span>
<span class="check">✔</span>
</li>
{{/ for }}
</ul>
</div>
<div class="bottom">
{{# if(this.route) }}
<div class="route-selected">
<small>Route {{ this.route.rt }}:</small> {{ this.route.rtnm }}
{{# if(this.vehiclesPromise.isRejected) }}
<div class="error-message">No vehicles available for this route</div>
{{/ if }}
</div>
{{/ if }}
<google-map-view/>
</div>
`;
static props = {
title: {
default: "Chicago CTA Bus Tracker"
},
routesPromise: {
get default() {
return fetch(proxyUrl + getRoutesEnpoint)
.then(response => response.json())
.then(data => data["bustime-response"].routes);
}
},
route: type.Any,
vehiclesPromise: type.Any
};
pickRoute(route) {
this.route = route;
this.vehiclesPromise = fetch(
proxyUrl + getVehiclesEndpoint + "&rt=" + route.rt
)
.then(response => response.json())
.then(data => {
if (data["bustime-response"].error) {
return Promise.reject(data["bustime-response"].error[0]);
} else {
return data["bustime-response"].vehicle;
}
});
}
}
customElements.define("bus-tracker", BusTracker);
Set markers for vehicle locations
The problem
In this section, we will:
- Show markers at bus locations when the user clicks a route.
We will do this by:
- Passing the
vehicles
fromvehiclePromise
to<google-map-view>
. - Listening when
vehicles
changes and creating google mapMarker
s.
What you need to know
childProp:from can set a component's property from another value:
<google-map-view aProp:from="scopeValue"/>
The component can listen to events fired by its properties like:
import { StacheElement, type } from "can"; class MyComponent extends StacheElement { static props = { // ... vehicles: type.Any }; connected() { this.listenTo("vehicles", (ev, newVehicles) => { // ... }); } }
Use new google.maps.Marker to add a marker to a map like:
new google.maps.Marker({ position: { lat: parseFloat(vehicle.lat), lng: parseFloat(vehicle.lon) }, map: this.map });
The solution
Update the JavaScript tab to:
import { StacheElement, type } from "//unpkg.com/can@6/core.mjs";
const proxyUrl = "https://can-cors.herokuapp.com/";
const token = "?key=piRYHjJ5D2Am39C9MxduHgRZc&format=json";
const apiRoot = "http://www.ctabustracker.com/bustime/api/v2/";
const getRoutesEnpoint = apiRoot + "getroutes" + token;
const getVehiclesEndpoint = apiRoot + "getvehicles" + token;
class GoogleMapView extends StacheElement {
static get view() {
return `<div this:to="this.mapElement" class="gmap"></div>`;
}
static get props() {
return {
map: type.Any,
mapElement: type.maybeConvert(HTMLElement),
vehicles: type.Any
};
}
connected() {
googleAPI.then(() => {
this.map = new google.maps.Map(this.mapElement, {
zoom: 10,
center: {
lat: 41.881,
lng: -87.623
}
});
});
this.listenTo("vehicles", (ev, newVehicles) => {
if (newVehicles) {
newVehicles.map(vehicle => {
return new google.maps.Marker({
position: {
lat: parseFloat(vehicle.lat),
lng: parseFloat(vehicle.lon)
},
map: this.map
});
});
}
});
}
}
customElements.define("google-map-view", GoogleMapView);
class BusTracker extends StacheElement {
static view = `
<div class="top">
<div class="header">
<h1>{{this.title}}</h1>
{{# if(this.routesPromise.isPending) }}<p>Loading routes…</p>{{/ if }}
{{# if(this.vehiclesPromise.isPending) }}<p>Loading vehicles…</p>{{/ if }}
</div>
<ul class="routes-list">
{{# for(route of this.routesPromise.value) }}
<li on:click="this.pickRoute(route)" {{# eq(route, this.route) }}class="active"{{/ eq }}>
<span class="route-number">{{ route.rt }}</span>
<span class="route-name">{{ route.rtnm }}</span>
<span class="check">✔</span>
</li>
{{/ for }}
</ul>
</div>
<div class="bottom">
{{# if(this.route) }}
<div class="route-selected">
<small>Route {{ this.route.rt }}:</small> {{ this.route.rtnm }}
{{# if(this.vehiclesPromise.isRejected) }}
<div class="error-message">No vehicles available for this route</div>
{{/ if }}
</div>
{{/ if }}
<google-map-view vehicles:from="this.vehiclesPromise.value"/>
</div>
`;
static props = {
title: {
default: "Chicago CTA Bus Tracker"
},
routesPromise: {
get default() {
return fetch(proxyUrl + getRoutesEnpoint)
.then(response => response.json())
.then(data => data["bustime-response"].routes);
}
},
route: type.Any,
vehiclesPromise: type.Any
};
pickRoute(route) {
this.route = route;
this.vehiclesPromise = fetch(
proxyUrl + getVehiclesEndpoint + "&rt=" + route.rt
)
.then(response => response.json())
.then(data => {
if (data["bustime-response"].error) {
return Promise.reject(data["bustime-response"].error[0]);
} else {
return data["bustime-response"].vehicle;
}
});
}
}
customElements.define("bus-tracker", BusTracker);
Clean up markers when locations change
The problem
In this section we will:
- Remove markers from previous routes.
- Update marker locations when the user clicks the overlay.
We will do this by:
- Storing the active list of markers
- Clearing the old active markers when the list of vehicles is updated.
- Calling
pickRoute
when someone clicks on theroute-selected
overlay.
What you need to know
- Use
marker.setMap(null)
to remove a marker from a map.
The solution
Update the JavaScript tab to:
import { StacheElement, type } from "//unpkg.com/can@6/core.mjs";
const proxyUrl = "https://can-cors.herokuapp.com/";
const token = "?key=piRYHjJ5D2Am39C9MxduHgRZc&format=json";
const apiRoot = "http://www.ctabustracker.com/bustime/api/v2/";
const getRoutesEnpoint = apiRoot + "getroutes" + token;
const getVehiclesEndpoint = apiRoot + "getvehicles" + token;
class GoogleMapView extends StacheElement {
static view = `<div this:to="this.mapElement" class="gmap"></div>`;
static props = {
map: type.Any,
mapElement: type.maybeConvert(HTMLElement),
vehicles: type.Any,
markers: type.Any
};
connected() {
googleAPI.then(() => {
this.map = new google.maps.Map(this.mapElement, {
zoom: 10,
center: {
lat: 41.881,
lng: -87.623
}
});
});
this.listenTo("vehicles", (ev, newVehicles) => {
if (Array.isArray(this.markers)) {
this.markers.forEach(marker => {
marker.setMap(null);
});
this.markers = null;
}
if (newVehicles) {
this.markers = newVehicles.map(vehicle => {
return new google.maps.Marker({
position: {
lat: parseFloat(vehicle.lat),
lng: parseFloat(vehicle.lon)
},
map: this.map
});
});
}
});
}
}
customElements.define("google-map-view", GoogleMapView);
class BusTracker extends StacheElement {
static view = `
<div class="top">
<div class="header">
<h1>{{this.title}}</h1>
{{# if(this.routesPromise.isPending) }}<p>Loading routes…</p>{{/ if }}
{{# if(this.vehiclesPromise.isPending) }}<p>Loading vehicles…</p>{{/ if }}
</div>
<ul class="routes-list">
{{# for(route of this.routesPromise.value) }}
<li on:click="this.pickRoute(route)" {{# eq(route, this.route) }}class="active"{{/ eq }}>
<span class="route-number">{{ route.rt }}</span>
<span class="route-name">{{ route.rtnm }}</span>
<span class="check">✔</span>
</li>
{{/ for }}
</ul>
</div>
<div class="bottom">
{{# if(this.route) }}
<div class="route-selected" on:click="pickRoute(route)">
<small>Route {{ this.route.rt }}:</small> {{ this.route.rtnm }}
{{# if(this.vehiclesPromise.isRejected) }}
<div class="error-message">No vehicles available for this route</div>
{{/ if }}
</div>
{{/ if }}
<google-map-view vehicles:from="this.vehiclesPromise.value"/>
</div>
`;
static props = {
title: {
default: "Chicago CTA Bus Tracker"
},
routesPromise: {
get default() {
return fetch(proxyUrl + getRoutesEnpoint)
.then(response => response.json())
.then(data => data["bustime-response"].routes);
}
},
route: type.Any,
vehiclesPromise: type.Any
};
pickRoute(route) {
this.route = route;
this.vehiclesPromise = fetch(
proxyUrl + getVehiclesEndpoint + "&rt=" + route.rt
)
.then(response => response.json())
.then(data => {
if (data["bustime-response"].error) {
return Promise.reject(data["bustime-response"].error[0]);
} else {
return data["bustime-response"].vehicle;
}
});
}
}
customElements.define("bus-tracker", BusTracker);
Result
When finished, you should see something like the following CodePen:
See the Pen CTA Bus Map (Medium) by Bitovi (@bitovi) on CodePen.