Saving Halloween 2020 with Azure Maps and Candy Caches

Jen Looper - Oct 21 '20 - - Dev Community

For a lot of kids, Halloween is a magical time, a favorite holiday where you can roam the streets, knocking on doors, demanding candy and treats, all in a cool costume. When I was a kid in Missouri (a state in the center of the USA), Halloween was a time when I got to hang out with my three older brothers and have a great time.

costumes

Witch or Bee this year? Let me know in the comments!

At some point, you're supposed to be too old for Halloween (NEVER), but when I was a student in Paris in the '90s we expats still dressed up and rode the Métro in wild costumes. Here in the town where I now live on the East coast of the US, it's cold in late October but still, kids dress up (maybe with a coat over their outfit) and come to collect their M&Ms. Halloween is awesome.

cat in a costume

EVERYONE loves Halloween!

But in 2020, Halloween is in jeopardy. How do you safely trick or treat in a pandemic? Each town seems to be making their own policy; in Wellesley (Massachusetts, near Boston, where I live), the official stance is that people can make their own choice to open their door to kids or not. This is a decision that needs some solid parental organization behind it to ensure that the experience is still fun.

Enter...Azure Maps!

I never saw a situation that need a custom mapping solution as badly as this one. So, in short order, I created a concept: Candy Caches, mapped on an Azure Static Web App using an Azure Map.

Alt Text

Building the Site

Making the web site was a snap. It took only a few steps to launch it as an Azure Static Web App:

Get your map key

Get an Azure Map Key by creating an Azure Map:
maps in the Azure portal

Scaffold your site

Use the Vue CLI to scaffold a basic Vue site with one page:
vue create halloween-maps. This app is built with Vue 2, but could easily be converted to Vue 3.

Commit this code to GitHub and then connect that repo to Azure Static Web Apps using the Azure Visual Studio Code Extension. A GitHub Action workflow file will be scaffolded for you provided with the name of a site where your app will live. Each time you commit to your repo, a fresh build will be kicked off.

Add a function

Add a function via the Azure Visual Studio Code Extension for Azure Static Web Apps. This function lives in its own folder called api and contains minimal code, just fetching the VUE_APP_MAP_KEY environment variable that is stored in Azure Static Web Apps:

module.exports = function (context) {
    let key = process.env['VUE_APP_MAP_KEY'];
    context.res = { body: key };
    context.done();

};
Enter fullscreen mode Exit fullscreen mode

functions in VSCode

Since we want to store our map key in the Azure Static Web Apps portal and not expose it on GitHub, we need a function to call it. Fortunately you can create all these things in the Azure Visual Studio Code Extension.

Store your map key in your Static Web App portal. For local development, use a local.settings.json file that's not committed to GitHub.

portal view

For local development of your site and its api, make sure you have a vue.config.js file available with the following server proxy so your API can run locally on port 7071.

devServer: {
        proxy: {
            '/api': {
                target: 'http://localhost:7071',
                ws: true,
                changeOrigin: true,
            },
        },
    },
Enter fullscreen mode Exit fullscreen mode

Build your map

Install the "azure-maps-control" package via npm and make sure to import the package into your app at the top of the <script> block:
import * as atlas from "azure-maps-control";

Then, implement your map:

First, set up a <div> in your <template>:

<div id="myMap"></div>
Enter fullscreen mode Exit fullscreen mode

Then, set up some initial data while the map draws to screen:

data: () => ({
    map: null,
    zoom: 13,//tweak this value to zoom the map in and out
    center: [-71.2757724, 42.3123219],//map centers here
    subKey: null,//subscription key
  }),
Enter fullscreen mode Exit fullscreen mode

Create a mounted lifecycle hook to get your API key from your function and then pass it to the function that draws your map:

async mounted() {
    try {
      //get the key
      const response = await axios.get("/api/getKey");
      this.subKey = response.data;
      //draw the map
      this.initMap(this.subKey);
    } catch (error) {
      console.error(error);
    }
  }
Enter fullscreen mode Exit fullscreen mode

The initMap function in the methods block starts the map building routine:

async initMap(key) {
      this.map = new atlas.Map("myMap", {
        center: this.center,
        zoom: this.zoom,
        view: "Auto",
        authOptions: {
          authType: "subscriptionKey",
          subscriptionKey: key,
        },
      });
      await this.buildMap();
    }
Enter fullscreen mode Exit fullscreen mode

Finally, in this large function, the map is constructed and injected into the myMap div:

buildMap() {
      let self = this;
      self.map.events.add("ready", function () {
        //Create a data source and add it to the map.
        let mapSource = new atlas.source.DataSource();
        self.map.sources.add(mapSource);
        mapSource.add(data);

        let popupSource = new atlas.source.DataSource();
        self.map.sources.add(popupSource);
        popupSource.add(data);
        //add a popup
        var symbolLayer = new atlas.layer.SymbolLayer(popupSource);

        //Add the polygon and line the symbol layer to the map.
        self.map.layers.add(symbolLayer);
        var popupTemplate =
          '<div style="padding:10px;color:white;font-size:11pt;font-weight:bold">{clue}<br/>{sitename}<br/>{refShort}<br/>{time}</div>';

        //Create a popup but leave it closed so we can update it and display it later.
        let popup = new atlas.Popup({
          pixelOffset: [0, -18],
          closeButton: true,
          fillColor: "rgba(0,0,0,0.8)",
        });

        //Add a hover event to the symbol layer.
        self.map.events.add("mouseover", symbolLayer, function (e) {
          //Make sure that the point exists.
          if (e.shapes && e.shapes.length > 0) {
            var content, coordinate;
            var properties = e.shapes[0].getProperties();
            content = popupTemplate
              .replace(/{clue}/g, properties.clue)
              .replace(/{sitename}/g, properties.sitename)
              .replace(/{refShort}/g, properties.refShort)
              .replace(/{time}/g, properties.time);
            coordinate = e.shapes[0].getCoordinates();

            popup.setOptions({
              content: content,
              position: coordinate,
            });

            popup.open(self.map);
          }
        });

        self.map.events.add("mouseleave", symbolLayer, function () {
          popup.close();
        });
      });
    }
Enter fullscreen mode Exit fullscreen mode

Notice the "symbol layer" that is constructed; these are the little popup flags that contain data about your candy caches.

The map is fed by a file in a format called 'GeoJSON'. This was a new format for me, but it works seamlessly once you understand how the data is laid out. Each point on the map is fed like thus:

//anonymized example
{
"type": "Feature",
"geometry": {
    "type": "Point",
    "coordinates": [-71.4567, 42.1234]
    },
"properties": {
    "clue": "Look for the scary archway!",
    "sitename": "Smith Residence",
    "refShort": "123 Weston Road",
    "time": "4-6pm"
    }
}
Enter fullscreen mode Exit fullscreen mode

You can determine coordinates of residences by saving a Postman call and feeding addresses into it. Use your subscription key to get the address's data:

Postman API call

WOO (Winning Others Over)

Now comes the hard part: convincing townspeople to participate in creating these contact-less candy caches and registering for the web site. I reached out to our local paper (the Swellesley Report editor, Bob Brown, is a pal) and to our town's Facebook group, "What's Up, Wellesley" and garnered a lot of interest! By creating a form, I have set up a process whereby townspeople can tell me their schedule, location, and clue and I can register their cache. We have over ten caches listed, and more on the way.

Want to create your own candy cache? The full repo is here: Halloween Maps. Don't be scared! Follow along as our site grows by visiting it. Tell me more about how you are celebrating your holidays this weird, weird year by adding a note in the comments.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player