DoneJS StealJS jQuery++ FuncUnit DocumentJS
3.14.1
5.0.0 4.3.0 2.3.35
  • About
  • Guides
  • API Docs
  • Community
  • Contributing
  • Bitovi
    • Bitovi.com
    • Blog
    • Design
    • Development
    • Training
    • Open Source
    • About
    • Contact Us
  • About
  • Guides
    • experiment
      • Chat Guide
      • TodoMVC Guide
      • ATM Guide
    • getting started
      • Setting Up CanJS
      • Reading the Docs (API Guide)
    • recipes
      • Credit Card Guide (Advanced)
      • Credit Card Guide (Simple)
      • CTA Bus Map (Medium)
      • File Navigator Guide (Advanced)
      • File Navigator Guide (Simple)
      • Playlist Editor (Advanced)
      • Signup and Login (Simple)
      • TodoMVC with StealJS
    • upgrade
      • Migrating to CanJS 3
      • Using Codemods
  • API Docs
  • Community
  • Contributing
  • GitHub
  • Twitter
  • Chat
  • Forum
  • News
Bitovi

CTA Bus Map (Medium)

  • Edit on GitHub

This guide walks you through showing Chicago Transit Authority (CTA) bus locations on a Google Map.

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:

JS Bin on jsbin.com

To use the widget:

  1. Click a Bus Route.
  2. Explore the markers added to the Google Map showing the bus locations for that route.
  3. Click the route name overlay to refresh the bus locations.

The following sections are broken down 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 CLONING THE FOLLOWING JS BIN:

Click the JS Bin button. The JSBin will open in a new window. In that new window, under File, click Clone.

CanJS Bus Demo on jsbin.com

This JS Bin 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 JSBin 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 instances of a ViewModel to manage the behavior of a View. A ViewModel type is defined, an instance of it is created and passed to a View as follows:

    // Define the ViewModel type
    var MyViewModel = can.DefineMap.extend("MyViewModel",{
     ...      
    })
    // Create an instance of the ViewModel
    var viewModel = new MyViewModel();
    // Get a View
    var view = can.stache.from("my-view");
    // Render the View with the ViewModel instance
    var frag = view(viewModel);
    document.body.appendChild(frag);
    
  • CanJS uses can-stache to render data in a template and keep it live. Templates can be authored in <script> tags like:

    <script type="text/stache" id="app-view">
      TEMPLATE CONTENT
    </script>
    

    A can-stache template uses {{key}} "magic tags" to insert data into the HTML output like:

    <script type="text/stache" id="app-view">
      {{something.name}}
    </script>
    
  • Load a template from a <script> tag with can.stache.from like:

    var template = can.stache.from(SCRIPT_ID);
    
  • Render the template with data into a documentFragment like:

    var frag = template({
      something: {name: "Derek Brunson"}
    });
    
  • Insert a fragment into the page with:

    document.body.appendChild(frag);
    

Loading Google Maps API

The following loads Google Maps API:

<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:

var apiRoot = "http://www.ctabustracker.com/bustime/api/v2/"

The API needs a token as part of the request:

var token = "?key=piRYHjJ5D2Am39C9MxduHgRZc&format=json";

However, the API does not support cross origin requests. Therefore, we will request data using a proxy hosted at:

var 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 ViewModel 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 viewModel like:
    viewModel.title = "TITLE UPDATED"
    

YOUR TITLE HERE

What you need to know

  • A can-stache template uses {{key}} magic tags to insert data into the HTML output like:

    {{someValue}}
    

    These values come from a ViewModel or Model.

  • The value property definition can return the initial value of a property like:

    var AppViewModel = can.DefineMap.extend({
      someValue: {
        value: "This string"
      }  
    });
    new AppViewModel().someValue //-> "This string"
    

How to verify it works

Run the folowing in the Console tab:

viewModel.title = "TITLE UPDATED"

You should see the title update.

The solution

Update the view in the HTML tab to:

<div class="top">
  <div class="header">
    <h1>{{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>

Update the JavaScript tab to:

var proxyUrl = "https://can-cors.herokuapp.com/";
var token = "?key=piRYHjJ5D2Am39C9MxduHgRZc&format=json";
var apiRoot = "http://www.ctabustracker.com/bustime/api/v2/"
var getRoutesEnpoint = apiRoot + "getroutes" + token;
var getVehiclesEndpoint = apiRoot + "getvehicles" + token;

var BusTrackerVM = can.DefineMap.extend({
  title: {
    value: "Chicago CTA Bus Tracker"
  }
});

var viewModel = new BusTrackerVM();

var view = can.stache.from("app-view");
var frag = view(viewModel);
document.body.appendChild(frag);

List bus routes

The problem

In this section, we will:

  • Load and list bus routes.
  • Show <p>Loading routes…</p> while loading routes.

List Bus Routes

We will do this by:

  • Store the promise of bus routes in a routesPromise property.
  • routesPromise will resolve to an Array 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 value property definition can return the initial value of a property like:

    var AppViewModel = can.DefineMap.extend({
      myProperty: {
        value: function(){
          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 and getRoutesEnpoint 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"}:

    var resultPromise = outerPromise.then(function(data){
        return data.innerData;
    });
    
  • Use {{#if(value)}} to do if/else branching in can-stache.

  • Use {{#each(value)}} 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: {{#each(somePromise.value)}}.

The solution

Update the view in the HTML tab to:

<div class="top">
  <div class="header">
    <h1>{{title}}</h1>
    {{#if(routesPromise.isPending)}}<p>Loading routes…</p>{{/if}}
  </div>
  <ul class="routes-list">
    {{#each(routesPromise.value)}}
      <li>
        <span class="route-number">{{this.rt}}</span>
        <span class="route-name">{{this.rtnm}}</span>
        <span class="check">✔</span>
      </li>
    {{/each}}
  </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>

Update the JavaScript tab to:

var proxyUrl = "https://can-cors.herokuapp.com/";
var token = "?key=piRYHjJ5D2Am39C9MxduHgRZc&format=json";
var apiRoot = "http://www.ctabustracker.com/bustime/api/v2/"
var getRoutesEnpoint = apiRoot + "getroutes" + token;
var getVehiclesEndpoint = apiRoot + "getvehicles" + token;

var BusTrackerVM = can.DefineMap.extend({
  title: {
    value: "Chicago CTA Bus Tracker"
  },
  routesPromise: {
    value() {
      return fetch(proxyUrl + getRoutesEnpoint)
        .then(response => response.json())
        .then(data => data["bustime-response"].routes);
    }
  }
});

var viewModel = new BusTrackerVM();

var view = can.stache.from("app-view");
var frag = view(viewModel);
document.body.appendChild(frag);

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:

    var AppViewModel = can.DefineMap.extend({
      myProperty: "any"  
    });
    var 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 busses, 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 view in the HTML tab to:

<div class="top">
  <div class="header">
    <h1>{{title}}</h1>
    {{#if(routesPromise.isPending)}}<p>Loading routes…</p>{{/if}}
  </div>
  <ul class="routes-list">
    {{#each(routesPromise.value)}}
      <li on:click="pickRoute(this)" {{#eq(this, route)}}class="active"{{/eq}}>
        <span class="route-number">{{this.rt}}</span>
        <span class="route-name">{{this.rtnm}}</span>
        <span class="check">✔</span>
      </li>
    {{/each}}
  </ul>
</div>
<div class="bottom">
  {{#if(route)}}
    <div class="route-selected">
      <small>Route {{route.rt}}:</small> {{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>

Update the JavaScript tab to:

var proxyUrl = "https://can-cors.herokuapp.com/";
var token = "?key=piRYHjJ5D2Am39C9MxduHgRZc&format=json";
var apiRoot = "http://www.ctabustracker.com/bustime/api/v2/"
var getRoutesEnpoint = apiRoot + "getroutes" + token;
var getVehiclesEndpoint = apiRoot + "getvehicles" + token;

var BusTrackerVM = can.DefineMap.extend({
  title: {
    value: "Chicago CTA Bus Tracker"
  },
  routesPromise: {
    value() {
      return fetch(proxyUrl + getRoutesEnpoint)
        .then(response => response.json())
        .then(data => data["bustime-response"].routes);
    }
  },
  route: '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 );
        }
      });
  }
});

var viewModel = new BusTrackerVM();

var view = can.stache.from("app-view");
var frag = view(viewModel);
document.body.appendChild(frag);

Show when busses 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 busses 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:
    var 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 the reason as {name: "inner"}:
    var resultPromise = outerPromise.then(function(data){
        return Promise.reject(data.innerData);
    });
    resultPromise.catch(function(reason){
        reason.name //-> "inner"
    });
    

The solution

Update the view in the HTML tab to:

<div class="top">
  <div class="header">
    <h1>{{title}}</h1>
    {{#if(routesPromise.isPending)}}<p>Loading routes…</p>{{/if}}
    {{#if(vehiclesPromise.isPending)}}<p>Loading vehicles…</p>{{/if}}
  </div>
  <ul class="routes-list">
    {{#each(routesPromise.value)}}
      <li on:click="pickRoute(this)" {{#eq(this, route)}}class="active"{{/eq}}>
        <span class="route-number">{{this.rt}}</span>
        <span class="route-name">{{this.rtnm}}</span>
        <span class="check">✔</span>
      </li>
    {{/each}}
  </ul>
</div>
<div class="bottom">
  {{#if(route)}}
    <div class="route-selected">
      <small>Route {{route.rt}}:</small> {{route.rtnm}}
      {{#if(vehiclesPromise.isRejected)}}
        <div class="error-message">No vehicles available for this route</div>
      {{/if}}
    </div>
  {{/if}}
  <div class='gmap'>Bus count: {{vehiclesPromise.value.length}}</div>
</div>

Update the JavaScript tab to:

var proxyUrl = "https://can-cors.herokuapp.com/";
var token = "?key=piRYHjJ5D2Am39C9MxduHgRZc&format=json";
var apiRoot = "http://www.ctabustracker.com/bustime/api/v2/"
var getRoutesEnpoint = apiRoot + "getroutes" + token;
var getVehiclesEndpoint = apiRoot + "getvehicles" + token;

var BusTrackerVM = can.DefineMap.extend({
  title: {
    value: "Chicago CTA Bus Tracker"
  },
  routesPromise: {
    value() {
      return fetch(proxyUrl + getRoutesEnpoint)
        .then(response => response.json())
        .then(data => data["bustime-response"].routes);
    }
  },
  route: 'any',
  vehiclesPromise: '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;
        }
      });
  }
});

var viewModel = new BusTrackerVM();

var view = can.stache.from("app-view");
var frag = view(viewModel);
document.body.appendChild(frag);

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 can-component element 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 can-component to create custom elements. Start by extending Component with the tag of the element:

    can.Component.extend({
      tag: "google-map-view"
    });
    

    Next, provide the HTML can-stache template with the content you want to insert within the element.

    can.Component.extend({
      tag: "google-map-view",
      view: stache(`<div class='gmap'/>`)
    });
    

    Any values you want the custom element to hold must be defined on the ViewModel. If the ViewModel is a plain Object, that object will be used to extend DefineMap and create a new type. The following specifies a map property that can be any value:

    can.Component.extend({
      tag: "google-map-view",
      view: stache(`<div class='gmap'/>`),
      ViewModel: {
        map: "any"
      }
    });
    

    A component's events object can be used to listen to events on the ViewModel or the element. If you want to know when the custom element is inserted, you can do it as follows:

    can.Component.extend({
      tag: "google-map-view",
      view: stache(`<div class='gmap'/>`),
      ViewModel: {
        map: "any"
      },
      events: {
        "{element} inserted": function(){
          this.viewModel //-> the ViewModel instance
          this.element //-> the <google-map-view> element
        }
      }
    });
    
  • 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 view in the HTML tab to:

<div class="top">
  <div class="header">
    <h1>{{title}}</h1>
    {{#if(routesPromise.isPending)}}<p>Loading routes…</p>{{/if}}
    {{#if(vehiclesPromise.isPending)}}<p>Loading vehicles…</p>{{/if}}
  </div>
  <ul class="routes-list">
    {{#each(routesPromise.value)}}
      <li on:click="pickRoute(this)" {{#eq(this, route)}}class="active"{{/eq}}>
        <span class="route-number">{{this.rt}}</span>
        <span class="route-name">{{this.rtnm}}</span>
        <span class="check">✔</span>
      </li>
    {{/each}}
  </ul>
</div>
<div class="bottom">
  {{#if(route)}}
    <div class="route-selected">
      <small>Route {{route.rt}}:</small> {{route.rtnm}}
      {{#if(vehiclesPromise.isRejected)}}
        <div class="error-message">No vehicles available for this route</div>
      {{/if}}
    </div>
  {{/if}}
  <google-map-view/>
</div>

Update the JavaScript tab to:

var proxyUrl = "https://can-cors.herokuapp.com/";
var token = "?key=piRYHjJ5D2Am39C9MxduHgRZc&format=json";
var apiRoot = "http://www.ctabustracker.com/bustime/api/v2/"
var getRoutesEnpoint = apiRoot + "getroutes" + token;
var getVehiclesEndpoint = apiRoot + "getvehicles" + token;

var BusTrackerVM = can.DefineMap.extend({
  title: {
    value: "Chicago CTA Bus Tracker"
  },
  routesPromise: {
    value() {
      return fetch(proxyUrl + getRoutesEnpoint)
        .then(response => response.json())
        .then(data => data["bustime-response"].routes);
    }
  },
  route: 'any',
  vehiclesPromise: '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;
        }
      });
  }
});

can.Component.extend({
  tag: "google-map-view",
  view: can.stache(`<div class='gmap'></div>`),
  ViewModel: {
    map: 'any'
  },
  events: {
    "{element} inserted": function() {
      googleAPI.then(() => {
        this.viewModel.map = new google.maps.Map(this.element.firstChild, {
          zoom: 10,
          center: {
            lat: 41.881,
            lng: -87.623
          }
        });
      });
    }
  }
});

var viewModel = new BusTrackerVM();

var view = can.stache.from("app-view");
var frag = view(viewModel);
document.body.appendChild(frag);

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 from vehiclePromise to <google-map-view>.
  • Listening when vehicles changes and creating google map Markers.

What you need to know

  • childProp:from can set a component's ViewModel from another value:

    <google-map-view viewModelProp:from="scopeValue"/>
    
  • The events can listen to changes in the ViewModel instance with: "{viewModel} propertyName" like:

    can.Component.extend({
      ...
      events: {
        "{viewModel} vehicles": function(viewModel, event, newVehicles) {
            // do stuff with the 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.viewModel.map
    });
    

The solution

Update the view in the HTML tab to:

<div class="top">
  <div class="header">
    <h1>{{title}}</h1>
    {{#if(routesPromise.isPending)}}<p>Loading routes…</p>{{/if}}
    {{#if(vehiclesPromise.isPending)}}<p>Loading vehicles…</p>{{/if}}
  </div>
  <ul class="routes-list">
    {{#each(routesPromise.value)}}
      <li on:click="pickRoute(this)" {{#eq(this, route)}}class="active"{{/eq}}>
        <span class="route-number">{{this.rt}}</span>
        <span class="route-name">{{this.rtnm}}</span>
        <span class="check">✔</span>
      </li>
    {{/each}}
  </ul>
</div>
<div class="bottom">
  {{#if(route)}}
    <div class="route-selected">
      <small>Route {{route.rt}}:</small> {{route.rtnm}}
      {{#if(vehiclesPromise.isRejected)}}
        <div class="error-message">No vehicles available for this route</div>
      {{/if}}
    </div>
  {{/if}}
  <google-map-view vehicles:from="vehiclesPromise.value"/>
</div>

Update the JavaScript tab to:

var proxyUrl = "https://can-cors.herokuapp.com/";
var token = "?key=piRYHjJ5D2Am39C9MxduHgRZc&format=json";
var apiRoot = "http://www.ctabustracker.com/bustime/api/v2/"
var getRoutesEnpoint = apiRoot + "getroutes" + token;
var getVehiclesEndpoint = apiRoot + "getvehicles" + token;

var BusTrackerVM = can.DefineMap.extend({
  title: {
    value: "Chicago CTA Bus Tracker"
  },
  routesPromise: {
    value() {
      return fetch(proxyUrl + getRoutesEnpoint)
        .then(response => response.json())
        .then(data => data["bustime-response"].routes);
    }
  },
  route: 'any',
  vehiclesPromise: '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;
        }
      });
  }
});

can.Component.extend({
  tag: "google-map-view",
  view: can.stache(`<div class='gmap'></div>`),
  ViewModel: {
    map: 'any',
    vehicles: 'any'
  },
  events: {
    "{viewModel} vehicles": function(vm, ev, newVehicles) {
      if ( newVehicles ) {
        newVehicles.map(vehicle => {
          return new google.maps.Marker({
            position: {
              lat: parseFloat(vehicle.lat),
              lng: parseFloat(vehicle.lon)
            },
            map: this.viewModel.map
          });
        });
      }
    },
    "{element} inserted": function() {
      googleAPI.then(() => {
        this.viewModel.map = new google.maps.Map(this.element.firstChild, {
          zoom: 10,
          center: {
            lat: 41.881,
            lng: -87.623
          }
        });
      });
    }
  }
});

var viewModel = new BusTrackerVM();

var view = can.stache.from("app-view");
var frag = view(viewModel);
document.body.appendChild(frag);

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 on the ViewModel
  • Clearing the old active markers when the list of vehicles is updated.
  • Calling pickRoute when someone clicks on the route-selected overlay.

What you need to know

  • Use marker.setMap(null) to remove a marker from a map.

The solution

Update the view in the HTML tab to:

<div class="top">
  <div class="header">
    <h1>{{title}}</h1>
    {{#if(routesPromise.isPending)}}<p>Loading routes…</p>{{/if}}
    {{#if(vehiclesPromise.isPending)}}<p>Loading vehicles…</p>{{/if}}
  </div>
  <ul class="routes-list">
    {{#each(routesPromise.value)}}
      <li on:click="pickRoute(this)" {{#eq(this, route)}}class="active"{{/eq}}>
        <span class="route-number">{{this.rt}}</span>
        <span class="route-name">{{this.rtnm}}</span>
        <span class="check">✔</span>
      </li>
    {{/each}}
  </ul>
</div>
<div class="bottom">
  {{#if(route)}}
    <div class="route-selected" on:click="pickRoute(route)">
      <small>Route {{route.rt}}:</small> {{route.rtnm}}
      {{#if(vehiclesPromise.isRejected)}}
        <div class="error-message">No vehicles available for this route</div>
      {{/if}}
    </div>
  {{/if}}
  <google-map-view vehicles:from="vehiclesPromise.value"/>
</div>

Update the JavaScript tab to:

var proxyUrl = "https://can-cors.herokuapp.com/";
var token = "?key=piRYHjJ5D2Am39C9MxduHgRZc&format=json";
var apiRoot = "http://www.ctabustracker.com/bustime/api/v2/"
var getRoutesEnpoint = apiRoot + "getroutes" + token;
var getVehiclesEndpoint = apiRoot + "getvehicles" + token;

var BusTrackerVM = can.DefineMap.extend({
  title: {
    value: "Chicago CTA Bus Tracker"
  },
  routesPromise: {
    value() {
      return fetch(proxyUrl + getRoutesEnpoint)
        .then(response => response.json())
        .then(data => data["bustime-response"].routes);
    }
  },
  route: 'any',
  vehiclesPromise: '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;
        }
      });
  }
});

can.Component.extend({
  tag: "google-map-view",
  view: can.stache(`<div class='gmap'></div>`),
  ViewModel: {
    map: 'any',
    vehicles: 'any',
    markers: 'any'
  },
  events: {
    "{viewModel} vehicles": function(vm, 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.viewModel.map
          });
        });
      }
    },
    "{element} inserted": function() {
      googleAPI.then(() => {
        this.viewModel.map = new google.maps.Map(this.element.firstChild, {
          zoom: 10,
          center: {
            lat: 41.881,
            lng: -87.623
          }
        });
      });
    }
  }
});

var viewModel = new BusTrackerVM();

var view = can.stache.from("app-view");
var frag = view(viewModel);
document.body.appendChild(frag);

CanJS is part of DoneJS. Created and maintained by the core DoneJS team and Bitovi. Currently 3.14.1.

On this page

Get help

  • Chat with us
  • File an issue
  • Ask questions
  • Read latest news