Tuesday, 19 June, 2018 UTC


Summary

As a web developer, you sometimes find yourself in a position where you are required to implement a map. Your first choice is to use Google Maps, right?
This looks okay. However, you may be required to overlay additional information over the map with the help of markers. You can use this method, or you can find a better solution that allows you to create markers inside an indoor 3D map! How cool is that? With indoor markers, you can provide unique experiences for users where they will be able to access information and interact with UIs right inside the map.
In this tutorial, we'll create two demos illustrating the power of WRLD maps. You'll learn how to create custom apps that can overlay real-time information over a 3D map. In the first demo, we'll add interactive markers to an existing indoor map of a mall. In the second demo, we'll place colored polygons over parking areas, indicating capacity.
You can find the completed project for both demos in this GitHub repository.
Prerequisites
For this article, you only need to have a fundamental understanding of the following topics:
  • JavaScript DOM
  • ES6 Syntax
  • ES6 Modules
I'll assume this is your first time using WRLD maps. However, I do recommend you at least have a quick read of the article:
  • Building Dynamic 3D Maps
You'll also need a recent version of Node.js and npm installed on your system (at the time of writing, 8.10 LTS is the latest stable version). For Windows users, I highly recommend you use Git Bash or any other terminal capable of handling basic Linux commands.
This tutorial will use yarn for package installation. If you prefer to use npm, please refer to this guide if you are unfamiliar with yarn commands.
Acquire an API Key
Before you get started, you'll need to create a free account on WRLD. Once you've logged in and verified your email address, you'll need to acquire an API key. For detailed instructions on how to acquire one, please check out the Getting Started section on Building Dynamic 3D Maps where it's well documented.
Approach to Building the Map
The creation of WRLD maps is a major technological achievement with great potential benefits for many industries. There are two main ways of expanding the platform's capabilities:
  • Using built-in tools, e.g. Map Designer and Places Designer
  • Building a custom app
Let me break down how each method can be used to achieve the desired results.

1. Using Map Designer and Places Designer

For our first demo, we can use Places Designer to create Store Cards. This will require us to create a Collection Set where all Point of Interest markers will be held. This set can be accessed both within the WRLD ecosystem, and externally via the API key. We can pass this data to a custom map created using the Map Designer. With this tool, we can share the map with others using its generated link. If you would like to learn more about the process, please watch the video tutorials on this YouTube playlist.
The beauty of this method is that no coding is required. However, in our case, it does have limitations:
  • Restrictive UI design - we can only use the UI that comes with Places Designer
  • Restrictive data set - we can't display additional information beyond what is provided
In order to overcome these limitations, we need to approach our mall map challenge using the second method.

2. Building a Custom App

Building custom apps is the most flexible option. Although it takes some coding effort, it does allow us to comprehensively tap into the wealth of potential provided by the WRLD platform. By building a custom app, we can create our own UI, add more fields and access external databases in real-time. This is the method that we'll use for this tutorial.
Building the App
Let's first create a basic map, to which we'll add more functionality later. Head over to your workspace directory and create a new folder for your project. Let's call it mall-map.
Open the mall-map folder in your code editor. If you have VSCode, access the terminal using Ctrl + ` and execute the following commands inside the project directory:
# Initialize package.json
npm init -f

# Create project directories
mkdir src
mkdir src/js src/css

# Create project files
touch src/index.html
touch src/js/app.js
touch src/css/app.css
touch env.js
This is how your project structure should look:
Now that we have our project structure in place, we can begin writing code. We'll start with index.html. Insert this code:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <link rel="stylesheet" href="./css/app.css" />
  <title>Shopping Mall</title>
</head>
<body>
  <div id="map"></div>
  <script src="js/app.js"></script>
</body>
</html>
Next, let's work on css/app.css. I'm providing the complete styling for the entire project so that we don't have to revisit this file again. In due time you'll understand the contents as you progress with the tutorial.
@import "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.0.1/leaflet.css";
@import "https://cdn-webgl.wrld3d.com/wrldjs/addons/resources/latest/css/wrld.css";
@import "https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.3.0/semantic.min.css";

html,
body {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;
}

#map {
  width: 100%;
  height: 100%;
  background-color: #000000;
}

/* -------- POPUP CONTENT -------- */
.main-wrapper > .segment {
  padding: 0px;
  width: 300px;
}

.contacts > span {
  display: block;
  padding-top: 5px;
}
Now we need to start writing code for app.js. However, we need a couple of node dependencies:
yarn add wrld.js axios
As mentioned earlier, we'll be taking advantage of modern JavaScript syntax to write our code. Hence, we need to use babel to compile our modern code to a format compatible with most browsers. This requires installing babel dependencies and configuring them via a .babelrc file. Make sure to install them as dev-dependencies.
yarn add babel-core babel-plugin-transform-runtime babel-runtime --dev
touch .babelrc
Copy this code to the .babelrc file:
{
  "plugins": [
    [
      "transform-runtime",
      {
        "polyfill": false,
        "regenerator": true
      }
    ]
  ]
}
We'll also need the following packages to run our project:
  • Parcel bundler - it's like a simplified version of webpack with almost zero configuration
  • JSON Server - for creating a dummy API server
Install the packages globally like this:
yarn global add parcel-bundler json-server

# Alternative command for npm users
npm install -g parcel-bundler json-server
That's all the node dependencies we need for our project. Let's now write some JavaScript code. First, supply your WRLD API key in env.js:
module.exports = {
  WRLD_KEY: '<put api key here>',
 };
Then open js/app.js and copy this code:
const Wrld = require('wrld.js');
const env = require('../../env');

const keys = {
  wrld: env.WRLD_KEY,
};

window.addEventListener('load', async () => {
  const map = await Wrld.map('map', keys.wrld, {
    center: [56.459733, -2.973371],
    zoom: 17,
    indoorsEnabled: true,
  });
});
The first three statements are pretty obvious. We've put all our code inside the window.addEventListener function. This is to ensure our code is executed after the JavaScript dependencies, that we'll specify later in index.html, have loaded. Inside this function, we've initialized the map by passing several parameters:
  • map - the ID of the div container we specified in index.html
  • keys.wrld - API key
  • center - latitude and longitude of the Overgate Mall located in Dundee, Scotland
  • zoom - elevation
  • indoorsEnabled - allow users to access indoor maps
Let's fire up our project. Go to your terminal and execute:
parcel src/index.html
Wait for a few seconds for the project to finish bundling. When it's done, open your browser and access localhost:1234. Depending on your Internet speed, the map shouldn't take too long to load.
Beautiful, isn't it? Feel free to click the blue icon. It will take you indoors. Navigate around to see the different stores. However, you'll soon realize that you can't access other floors. There's also no button for exiting the indoor map. Let's fix that in the next chapter.
Create Indoor Controls
To allow users to switch between different floors, we'll provide them with a control widget that will allow them to do this. Simply add the following scripts to the head section of the public/index.html file:
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
<script src="https://cdn-webgl.wrld3d.com/wrldjs/addons/indoor_control/latest/indoor_control.js"></script>
Still within the html file, add this div in the body section, right before the #map div:
<div id="widget-container" class="wrld-widget-container"></div>
Now let's update js/app.js to initialize the widget. Place this code right after the map initialization section:
const indoorControl = new WrldIndoorControl('widget-container', map);
Now refresh the page, and click the 'Enter Indoors' icon. You should have a control widget that will allow you to switch between floors. Just drag the control up and down to fluidly move between floors.
Amazing, isn't it? Now let's see how we can make our map a little bit more convenient for our users.
Enter Indoors Automatically
Don't you find it a bit annoying that every time we need to test our map, we need to click the 'Indoors' icon? Users may start navigating to other locations which is not the intention of this app. To fix this, we need to navigate indoors automatically when the app starts without any user interaction. First, we require the indoor map id to implement this feature. We can find this information from the indoormapenter event. You can find all Indoor related methods here.
Add the following code in the js/app.js file.
...
// Place this code right after the Wrld.map() statement
map.indoors.on('indoormapenter', async (event) => {
  console.log(event.indoorMap.getIndoorMapId());
});
...
Refresh the page then check out your console. You should get this ID printed out: EIM-e16a94b1-f64f-41ed-a3c6-8397d9cfe607. Let's now write the code that will perform the actual navigation:
const indoorMapId = 'EIM-e16a94b1-f64f-41ed-a3c6-8397d9cfe607';

map.on('initialstreamingcomplete', () => {
  map.indoors.enter(indoorMapId);
});
After saving the file, refresh the page and see what happens.
The indoor mall map should navigate automatically. Next, we'll look at how we can create cards for each store. But first, we need to determine where to source our data.
Mall Map Planning
To create store cards for our map, we need several items:
  • Exact Longitude/Latitude coordinates of a store
  • Store contact information and opening hours
  • Design template for the store card

Store Card Coordinates

To acquire Longitude/Latitude coordinates, we need to access maps.wrld3d.com. Wait for the map to finish loading then enter the address 56.459733, -2.973371 in the search box. Press enter and the map will quickly navigate to Overgate Mall. Click the blue indoor icon for Overgate Mall and you should be taken to the mall's indoor map. Once it's loaded, locate the 'Next' store and right-click to open the context menu. Click the 'What is this place? option. The coordinate popup should appear.
Click the 'Copy Coordinate' button. This will give you the exact longitude/latitude coordinates of the store. Save this location address somewhere temporarily.

Store Card Information

You'll also need to gather contact information from each store which includes:
  • image
  • description
  • phone
  • email
  • web
  • Twitter
  • opening times
You can source most of this information from Google. Luckily, I've already collected the data for you. For this tutorial, we'll only deal with four stores on the ground floor. To access the information, just create a folder at the root of the project and call it data. Next save this file from GitHub in the data folder. Make sure to save it as db.json. Here is a sample of the data we'll be using:
{
  "id":1,
  "title": "JD Sports",
  "lat": 56.4593425,
  "long": -2.9741433,
  "floor_id": 0,
  "image_url": "https://cdn-03.belfasttelegraph.co.uk/business/news/...image.jpg",
  "description":"Retail chain specialising in training shoes, sportswear & accessories.",
  "phone": "+44 138 221 4545",
  "email": "[email protected]",
  "web": "https://www.jdsports.co.uk/",
  "twitter": "@jdhelpteam",
  "tags": "sports shopping",
  "open_time":[
    { "day": "Mon",
      "time": "9:30am - 6:00pm"
    },]
}
The data is stored in an array labeled 'pois'. POI stands for Places of Interest. Now that we have the data available, we can easily make it accessible via an API REST point by running the JSON server. Just open a new terminal and execute the command:
json-server --watch data/db.json
It should take a few seconds for the API to start. Once it's fully loaded, you can test it with your browser at localhost:3000/pois. You can also fetch a single POI using this syntax:
- localhost:3000/pois/{id}
For example, localhost:3000/pois/3 should return a poi record with ID 3 in JSON format.

Store Card Design

We'll use a clean elegant theme to neatly display contact information and opening times using a couple of tabs. We'll create markers that will display a popup when clicked. This popup will have the following UI.
The code for this HTML design is a bit long to put here. You can view and download the file from this link. The design only has three dependencies:
  • Semantic UI CSS
  • jQuery
  • Semantic UI JS
Now that we have the data required and the design, we should be ready to start working on our indoor map.
Implementing Store Cards in Indoor Map
First let's create a service that will allow us to access data from the JSON REST APIs. This data will be used for populating the Store Cards with the necessary information. Create the file js/api-service.js and copy this code:
const axios = require('axios');

const client = axios.create({
  baseURL: 'http://127.0.0.1:3000',
  timeout: 1000,
});

module.exports = {
  getPOIs: async () => {
    try {
      const response = await client.get('/pois');
      return response.data;
    } catch (error) {
      console.error(error);
    }
    return [];
  },
  getPOI: async (id) => {
    try {
      const response = await client.get(`/pois/${id}`);
      return response.data;
    } catch (error) {
      console.error(error);
    }
    return {};
  },
}
Here we are making use of the library axios to request data from the JSON server.
Next, we'll convert our static HTML design for the Store Card to a format that will allow us to render data. We'll be using JsRender for this. We'll break down our static design into three templates:
  • Base Template - has containers for menu, info and time tabs.
  • Info Template - tab for store contact information.
  • Time Template - tab for store opening hours.
First, open index.html and add these scripts to the head section, right after the jQuery and indoor control scripts:
<head>
  ...
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jsrender/0.9.90/jsrender.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.3.0/semantic.min.js"></script>
  ...
</head>
Next, copy this section of code right before the widget-container div:
  ...
  <!-- Menu Tabs UI -->
 <script id="baseTemplate" type="text/x-jsrender">
    <div class="main-wrapper">
      <div class="ui compact basic segment">
        <div class="ui menu tabular"> </div>
        <div id="infoTab" class="ui tab active" data-tab="Info"></div>
        <div id="timeTab" class="ui tab" data-tab="Time"></div>
      </div>
    </div>
  </script>

  <!-- Info Data Tab -->
  <script id="infoTemplate" type="text/x-jsrender">
    <div class="ui card">
      <div class="image">
        <img src={{:image_url}}>
      </div>
      <div class="content">
        <div class="header">{{:title}}</div>
        <div class="description">
          {{:description}}
        </div>
      </div>
      <div class="extra content contacts">
        <span>
          <i class="globe icon"></i>
          <a href="{{:web}}" target="_blank">{{:web}}</a>
        </span>
        <span>
          <i class="mail icon"></i>
          {{:email}}
        </span>
        <span>
          <i class="phone icon"></i>
          {{:phone}}
        </span>
      </div>
    </div>
  </script>

  <!-- Opening Times Data Tab -->
  <script id="timeTemplate" type="text/x-jsrender">
    <table class="ui celled table">
      <thead>
        <tr>
          <th>Day</th>
          <th>Time</th>
        </tr>
      </thead>
      <tbody>
        {{for open_time}}
        <tr>
          <td>{{:day}}</td>
          <td>{{:time}}</td>
        </tr>
        {{/for}}
      </tbody>
    </table>
  </script>
  ...
This is how the full code for index.html should look.
Next, let's create another service that will manage the creation of Popups. Create the file js/popup-service.js and copy this code:
const Wrld = require('wrld.js');
const { getPOI } = require('./api-service');

const baseTemplate = $.templates('#baseTemplate');
const infoTemplate = $.templates('#infoTemplate');
const timeTemplate = $.templates('#timeTemplate');

const popupOptions = {
  indoorMapId: 'EIM-e16a94b1-f64f-41ed-a3c6-8397d9cfe607',
  indoorMapFloorIndex: 0,
  autoClose: true,
  closeOnClick: true,
  elevation: 5,
};
Let me explain each block step by step:
  • Block 1: WRLD is required for creating the Popup, getPOI function is required for fetching data
  • Block 2: The templates that we discussed earlier are loaded using jsrender
  • Block 3: Parameters that will be passed during Popup instantiation. Here is the reference documentation.
Next, let's add tab menus that will be used for switching tabs. Simply add this code to js/popup-service.js:
const createMenuLink = (linkName, iconClass) => {
  const link = document.createElement('a');
  link.className = 'item';
  const icon = document.createElement('i');
  icon.className = `${iconClass} icon`;
  link.appendChild(icon);
  link.appendChild(document.createTextNode(` ${linkName}`));
  link.setAttribute('data-tab', linkName);
  link.addEventListener('click', () => {
    $.tab('change tab', linkName);
    $('.item').toggleClass('active');
  });
  return link;
};

const createMenu = (menuParent) => {
  const infoLink = createMenuLink('Info', 'info circle');
  infoLink.className += ' active';
  menuParent.appendChild(infoLink);
  const timeLink = createMenuLink('Time', 'clock');
  menuParent.appendChild(timeLink);
};
You might be wondering why we are using a complicated method of creating menu links. Ideally, we should be able to create them using HTML, then add a small JavaScript script to activate the tabs. Unfortunately, this doesn't work within the context of a Popup. Instead, we need to create clickable elements using DOM manipulation methods.
The post How to Create a Mall Map with Real-time Data Using WRLD appeared first on SitePoint.