-
Notifications
You must be signed in to change notification settings - Fork 4
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.
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.
- Git
- Node.js (which comes with npm)
- IDE or text editor, such as
vim
, Sublime, Atom, or VS Code - terminal
Now, let's get started...
We'll begin by making the simplest app possible using a template and highlighting all the key components that make the app work.
- 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
- Run the app like so.
# Run the app
npm start
Congratulations! You have yourself an electron app. You should see something like this:
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 ofindex.html
Look through these files in detail and get to know some of the important parts by reading the comments that describe them.
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.
- 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>
- 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 updateindex_renderer.js
as follows. NOTE: replacemapzen-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
};
- 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);
}
- 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
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.
- In the
index_renderer.js
file at the end of theinitMap()
function add the following code. For more details on what theclick
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.
- Right after the
initMap()
function, create a new function that will at first simply print thelatitude
andlongitude
to the console.
function lookupLocation(lat, lng) {
console.log(lat, lng);
}
- Let's run the code at this point and watch the console window to see what happens when we click on the map.
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.
- Let's first include that wrapper in our
index_renderer.js
file. At the top of the file, after all the otherrequire
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.
- 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);
});
}
- 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
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.
- 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');
- Then we will make use of the
whosonfirst()
function we just imported inside the callback function ofsearch
. To make it easier to follow, let's just see what the entirelookupLocation()
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.
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.
- So let's put the highlight declaration at the top of the
index_renderer.js
file, along with the other global variables.
let highlight;
- 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();
}
- 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
});
});
}
- 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
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.
- 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
- Let's get rid of the global
api_key
variable. Starting in this section we're going to rely on theelectron-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 theindex_renderer.js
file.
const settings = require('electron-settings');
-
Now find and replace all usage of
api_key
withsetting.get('current_api_key')
. Great! -
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 thesrc
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);
}
- 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');
}
- 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);
- In the above code, we register for the
api_key_updated
event with a function calledupdateKey()
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();
}
- 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');
- 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);
- 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"
}
}
- 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!