Skip to content

How to Electron Workshop

Diana Shkolnikov edited this page Jun 12, 2017 · 22 revisions

Welcome to the How to Electron Workshop

The goal of the workshop is to familiarize the participants with the Electron framework and walk through creating a very simple Electron app with a map and Mapzen developer authentication.

Before we start

You'll need to have a few things installed on your machine in order to follow along in this workshop. They don't take much time to install, so don't worry.

Now, let's get started...

Create and run an empty Electron app

We'll begin by making the simplest app possible using a template and highlighting all the key components that make the app work.

  1. Clone and install the mapzen/electron-quick-start repository to your machine as follows:
# Clone the repository
git clone https://github.com/mapzen/electron-quick-start
# Go into the repository
cd electron-quick-start
# Install dependencies
npm install
  1. Run the app like so.
# Run the app
npm start

Congratulations! You have yourself an electron app. You should see something like this:

What makes an Electron app

Now, let's examine all the things we get out of the box. There are a few key files that we need to examine closely.

  • package.json - indicates which file is the entrypoint of the application and lists all the necessary dependencies
  • main.js - main process and entrypoint of the application, responsible for creating all browser windows that render HTML
  • index.html - an HTML webpage to render in a browser window, runs as a renderer process
  • index_renderer.js - JavaScript code to accompany the index.html page, also runs in the renderer process of index.html

Look through these files in detail and get to know some of the important parts by reading the comments that describe them.

Let's add a map!

We all know we want to put a map front-and-center so why wait. Mapzen.js makes it easy to add a map to anything that supports HTML and JS. Here goes.

It's easiest to follow along with any one of the awesome tutorials in Mapzen.js or Search documentations, such as add-search-to-a-map or create-a-basic-web-map-using-mapzenjs.

The gist of it is as follows.

  1. Add the map <div> and <style> boilerplate to the HTML page on which the map will be displayed.

This part will go at the end of the <head> ... </head> section.

    <!-- Mapzen.js -->
    <link rel="stylesheet" href="https://mapzen.com/js/mapzen.css">
    <script src="https://mapzen.com/js/mapzen.min.js"></script>
    <style>
      html,body{margin: 0; padding: 0;}
      #map-container {
        height: 100%;
        position: relative;
      }
      #map {
        height: 100%;
        width: 100%;
        position: absolute;
      }
    </style>

and this inside the <body> ... </body> section...

    <!-- THE MAP -->
    <div id="map-container">
      <div id="map"></div>
    </div>
  1. Now we'll need to add the JS code that initializes the map with an api_key and initial center and zoom level. So let's update index_renderer.js as follows. NOTE: replace mapzen-xxxxxx with your API key.

Add a global map variable just above the page loading handler function and a call to initMap() inside the loading handler.

const api_key = 'mapzen-xxxxxxx';
let map; // <--- global map variable

// handle window loading event
document.getElementById('body').onload = function () {
  console.log('good news: index window is loading!');  

  initMap(); // <--- initialize your map
};
  1. Now add the body of the initMap() function just below the loading handler, like so.
function initMap() {
  // Add a Mapzen API key
  L.Mapzen.apiKey = api_key;
  map = L.Mapzen.map('map', { maxZoom: 18, minZoom: 2 });
  
  // Set the center of the map to be the San Francisco Bay Area at zoom level 12
  map.setView([40, -90], 4);

  var geocoder = L.Mapzen.geocoder();
  geocoder.addTo(map);
}
  1. Now save all your files and start the application back up again by running npm start. If all went smoothly you should be looking at a beautiful map of the world that takes up the entire browser window of your Electron app.

If you didn't get the same results or just don't have time to follow along on your own, just check out the map-only branch to see the code.

git checkout map-only

Let's catch some clicks

In this step, we're going to detect when a user clicks on the map and do something useful with the location they clicked on. First, let's add an event handler for the click action.

  1. In the index_renderer.js file at the end of the initMap() function add the following code. For more details on what the click event contains, see the official Leaflet documentation.
  map.on('click', function (e) {
    lookupLocation(e.latlng.lat, e.latlng.lng);
  });

This code says that anytime the click event is detected, the code inside the handler function should be executed, which in this case simply calls the function lookupLocation() which we will define in the next step.

  1. Right after the initMap() function, create a new function that will at first simply print the latitude and longitude to the console.
function lookupLocation(lat, lng) {
  console.log(lat, lng);
}
  1. Let's run the code at this point and watch the console window to see what happens when we click on the map.

Reverse geocode the clicks

Now that we have the location of every click on the map, let's use the Mapzen Search API to lookup the region the location falls within. We'll be using a simple wrapper in the mapzen-util directory.

  1. Let's first include that wrapper in our index_renderer.js file. At the top of the file, after all the other require statements add the following.
const search = require('./mapzen-util/src/js/mapzenSearch');

That will load the wrapper and make a new function called search() for us to use in order to access Mapzen Search. To make use of the function we need to create an options object with all the information needed to send a successful request to the search engine.

  1. We'll need to make our lookupLocation() function look like this.
function lookupLocation(lat, lng) {
  console.log(lat, lng);

  const options = {
    host: 'https://search.mapzen.com',
    api_key: api_key,
    endpoint: 'reverse',
    params: {
      'point.lat': lat,
      'point.lon': lng,
      layers: 'region'
    }
  };
  search(options, function (err, results) {
    console.log(results);
  });
}
  1. Now we can again run our code and examine the console window as we click on the map. We should see something like this when expanding the log statement right after the lat/lng pair.

Again, this is a checkpoint you can check out in a branch by checking out the map-reverse branch.

git checkout map-reverse

Geometries with Who's on First

Congratulations on getting this far! Now let's connect the reverse geocoding results to the Who's on First data by grabbing the geometries for the regions the user is clicking on.

  1. Again, we've prepared a simple wrapper for the WOF data to make it more accessable in our app. We'll need to include that wrapper in our index_renderer.js file with the following line at top of the file.
const whosonfirst = require('./mapzen-util/src/js/whosonfirst');
  1. Then we will make use of the whosonfirst() function we just imported inside the callback function of search. To make it easier to follow, let's just see what the entire lookupLocation() function should now look like.
function lookupLocation(lat, lng) {
  console.log(lat, lng);

  const options = {
    api_key: api_key,
    endpoint: 'reverse',
    params: {
      'point.lat': lat,
      'point.lon': lng,
      layers: 'region'
    }
  };
  search(options, function (err, results) {
    console.log('reverse geocoding results', results);

    if (results.features.length < 1) {
      console.log('no reverse geocoding results');
      return;
    }    

    const wofOptions = {
      id: results.features[0].properties.source_id
    };
    whosonfirst(wofOptions, function (err, region) {
      console.log('region details', region);
    });
  });
}

The new code will check that there is at least one result being returned from reverse geocoding and then using the source_id of that first result to lookup the full record's data in WOF.

Draw polygons on the map

The last step is to draw the geometry we received from WOF on our map. We'll just use Leaflet's built in drawing capabilities for this step, but keep in mind there are better ways to draw data on the map using Tangram and scene files, as described in Rhonda Friberg's excellent blog series.

Here is a function that will draw the results of both the search and WOF queries on our map. It also requires that we add a new global variable at the top to keep track of the highlights we're drawing for each clicked region. We need to remove the previous one before drawing a new one to keep the map clutter free.

  1. So let's put the highlight declaration at the top of the index_renderer.js file, along with the other global variables.
let highlight;
  1. And let's define the addToMap() function at the very bottom of the same file, like so.
function addToMap(searchResult, wofResult) {
  // if previous highlight was drawn, remove it
  if (highlight) {
    map.removeLayer(highlight);
  }

  highlight = L.geoJson(wofResult, {
    style: function (feature) {
      return {
        weight: 1,
        color: 'purple',
        opacity: '0.7'
      };
    }
  }).addTo(map);
  
  highlight.bindPopup(searchResult.properties.label).openPopup();
}
  1. Now all that's left to do is call our new drawing function in the right place and with the right parameters. Again for the sake of simplicity, here's what the entire lookupLocation() function should look like once the drawing function call has been added.
function lookupLocation(lat, lng) {
  console.log(lat, lng);

  const options = {
    api_key: api_key,
    endpoint: 'reverse',
    params: {
      'point.lat': lat,
      'point.lon': lng,
      layers: 'region'
    }
  };
  search(options, function (err, results) {
    console.log('reverse geocoding results', results);

    if (results.features.length < 1) {
      console.log('no reverse geocoding results');
      return;
    }    

    const wofOptions = {
      id: results.features[0].properties.source_id
    };
    whosonfirst(wofOptions, function (err, region) {
      console.log('region details', region);

      addToMap(results.features[0], region); // <--- DRAWS THE RESULTS ON MAP
    });
  });
}
  1. And there you have it, folks! You are now the creators of an Electron application that takes advantage of Mapzen Vector Tiles, Tangram, Mapzen Search, and Who's on First. Not bad for an hour's worth of work! 👏 👏 👏

Again, this is a checkpoint you can check out in a branch by checking out the map-reverse-geometry branch.

git checkout map-reverse-geometry

Extra Credit

Mapzen Developer Authentication

So far we've just been hard-coding our API key inside the app, which works for a lot of applications, however, there are some use-cases that require the user to provide their own API key to be used in things such as batch search, for example. For these use-cases, we've implemented a very basic authentication wrapper and included it within the mapzen-util directory. Let's see how it can be used to add user login and authentication to our existing application.

  1. Let's start with the visual indicator of authentication: the user avatar! We can add a simple bar and avatar button to the top of our window. Here's what the index.html could look like with the avatar header in place.
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>My Awesome Electron App</title>

    <!-- Stylesheets -->
    <link rel="stylesheet" href="https://mapzen.com/common/styleguide/styles/styleguide.css">

    <!-- Mapzen.js -->
    <link rel="stylesheet" href="https://mapzen.com/js/mapzen.css">
    <script src="https://mapzen.com/js/mapzen.min.js"></script>
    <style>
      html,body{margin: 0; padding: 0;}
      #map-container {
        height: calc(100% - 55px);
        position: relative;
      }
      #map {
        height: 100%;
        width: 100%;
        position: absolute;
      }
      #header {
        height: 55px;
        width: 100%;
        background-color: #1e0e33;
        color: #f8f8f8;
        text-align: center;
        display: inline-block;
        vertical-align: middle;
        line-height: normal;
        line-height: 50px;
        width: 100%;
      }
    </style>

  </head>
  <body id="body">

    <!-- HEADER with user avatar -->
    <div id="header">
      <img id="user-avatar" class="avatar-byline">
    </div>

    <!-- THE MAP -->
    <div id="map-container">
      <div id="map"></div>
    </div>

  </body>

  <!-- Javascript -->
  <script src="https://mapzen.com/common/styleguide/scripts/mapzen-styleguide.min.js"></script>
  <script src="./index_renderer.js" charset="utf-8"></script>

</html>

With the UI components in place, let's hook them up to some JavaScript in our index_renderer.js

  1. Let's get rid of the global api_key variable. Starting in this section we're going to rely on the electron-settings module to keep track of all of our user-specific global variables, such as current API key and avatar image URL. Make sure this line is included at the top of the index_renderer.js file.
const settings = require('electron-settings');
  1. Now find and replace all usage of api_key with setting.get('current_api_key'). Great!

  2. We will also need a way to initialize and update the user avatar we just added to the top of the window. We can use the user-avatar setting value for the src of the image. We also need to attach a listener to the avatar image so clicking it brings up the User Profile window.

// if you have a user avatar on this page, this will bring up the user profile window
// when avatar is clicked on
function initUserAvatar() {
  const defaultAvatar = './mapzen-util/public/img/mapzen-logo.png';
  document.getElementById('user-avatar').src =  settings.get('user_avatar') || defaultAvatar;
  document.getElementById('user-avatar').addEventListener('click', showUserProfile);
}
  1. Now, let's create a function to trigger the display of the User Profile window. This can be accomplished by sending an event to the main process. The Electron ipcRenderer object allows renderer processes to communicate with the main process.
function showUserProfile() {
  // this is how you get the user profile window to be created and shown
  ipcRenderer.send('user');
}
  1. Next, we need to send out some events to the main process and register to listen for key updates via an incoming event from the main process. Here's the corresponding code. It should be placed right after the global variables at the top of the file.
// let main process know that you're interested in key update events
ipcRenderer.send('waiting_for_key_updates');
// listen for key update event and refresh the map and avatar
ipcRenderer.on('api_key_updated', updateKey);
  1. In the above code, we register for the api_key_updated event with a function called updateKey() which we now need to implement. This will be the place to update visual representation of the user, such as avatar on this window as well as anything that uses an API key. For our purposes, we need to update the avatar and reinitialize the map using the new key.
function updateKey() {
  initUserAvatar();
  initMap();
}
  1. At the top of the initMap() function, we'll need to add some code to clear the existing map before recreating to avoid exceptions.
function initMap() {
  // clear map if previously existed
  if (map) {
    // if previous highlight was drawn, remove it
    if (highlight) {
      map.removeLayer(highlight);
      highlight = null;
    }
    document.getElementById('map-container').innerHTML = '<div id="map"></div>';    
  }  

  // Add a Mapzen API key
  L.Mapzen.apiKey = settings.get('current_api_key');
  1. That's it for the renderer code. Now let's add some authentication magic to the main process to make it aware of what these messages from the renderer mean. Open up the main.js file and add the following code after the global variables have been declared and before other code.
const config = require('./config.json');
const auth = require('./mapzen-util/src/js/authMain');
auth.init(ipcMain);
  1. We will also need a new config.json file that will contain all the secret passwords and tokens required to perform authentication. There are excellent resources that you will want to familiarize yourself with when creating a new application. Note: the following links are both pointing to private repositories in the Mapzen org, so if they appear broken for you ask an admin to add you as a collaborator.

Once you've registered a new application or borrowed credentials from a friend you are ready to create the ./config.json file. It should look something like this.

{
  "auth": {
    "client_id": "some-very-long-string",
    "client_secret": "some-very-long-string",
    "redirect_uri": "http://localhost:9000/mapzen/auth/callback",
    "host": "https://mapzen.com"
  }
}
  1. We're ready to run the application again and see our user authentication in action!

First thing you'll see this time around is the authentication page, like so.

Once you've successfully logged in you should see your user profile with all the available keys.

Finally, after selecting a key to be used by the application you can proceed to the map window. You'll notice your avatar proudly displayed in the header.

The final code can be found in the map-reverse-geometry-auth branch. Beware that you will need to create a config.json file as described in step 10 before this branch will run successfully.

git checkout map-reverse-geometry-auth

That's all folks!