Dynamic Angular Map Markers with Google Overlay View | Reonomy - Commercial Real Estate Data Made Easy

Dynamic Angular Map Markers with Google Overlay View

A big part of working with property data at Reonomy is visualizing locations on maps, and specifically creating a map experience that matches our brand UI. So, today we are going to take a look at creating lovely, flexible, maintainable custom markers on a Google map using Angular. We’ll do this minimally, without a lot of bells and whistles, and most importantly, without the use of angular-google-maps.

What happened was what usually happens when one uses any framework, library, or plugin for some period of time: there comes that “oh, they don’t support that feature” moment, and to get the app shipped you either need to write something heinous and embarrassing to work around it or be forced to live without that feature (and tell product, marketing, and the execs they have to live without that feature too…haha, yeah right). Much of the time, this leads us to that hard question: how much of a benefit really was this extra code and extra layer of abstraction?

Creating custom google map markers easily falls into this category. Marker customization in the google API using google.maps.Marker turns out to be quite limiting: You can pretty much either swap the image icon out for your own, or draw your own vector path icon. What’s the fun in that? Where’s the CSS, or the ability to toggle classes on a marker? It’s not there, I’m afraid, and using angular-google-maps to customize markers suffers the same limitations.

Enter our hero, google.maps.OverlayView.

This will give us the power we need to create and manage custom map markers (and anything you would ever want to draw on a map, frankly), and certainly without need for any external libraries.

Our final fiddle will look like this:
http://jsfiddle.net/Lt7aP/1453/

With our app structure as such:

angular.module(‘myApp’,[])
.directive(‘mapMarker’, function(){…})
.controller(‘MapMarkerCtrl’, function(){…})
.controller(‘MapCtrl’, function(){…})
.controller(‘LocationsCtrl’, function(){…})
.service(‘mapMarkerConstructor’, function(){…})
.factory(‘locationsModel’, function(){…})

To get it up and running we’ll go through each of these steps:

1. Getting the map on the page

Thankfully, the google API makes this part simple and straight forward.

new google.maps.Map(mapElement, mapOptions);

That’s the jist, at least. What might not be so straight forward is how you architect this in angular. In larger applications, especially when you know you’ll be maintaining only a single map instance, it would be more proper to support it with a service (either .factory or .service, which is how angular-google-maps handles it). Namely because a google map is not really supposed to be destroyed in a single-page app. However for the sake of simplicity in this example, we will wrap our google maps call in a single controller:

.controller(‘MapCtrl’, function($scope, $element) {
const mapEl = $element.find(‘gmap’)[0];
const mapOptions = {
zoom: 14,
center: {lat: 34.075328, lng: -118.330432}
};
const gmap = new google.maps.Map(mapEl, mapOptions);
$scope.gmap = gmap;
})

Make sure you’ve got the google maps API script linked in the document <head>, your base Angular app module working, and with the following HTML/CSS, you’ll have yourself a map:

<figure ng-controller=“MapCtrl”>
<gmap></gmap>
</figure>

figure {
position: absolute;
top: 0;
left: 0;
margin: 0;
width: 50%;
height: 50%;
}
gmap {
display: block;
height: 100%;
}

Note that we wrapped our gmap element in a figure – you’ll typically need a wrapping element around your map because 1) The wrapping element needs an explicit width and height for the map to fill, and 2) google sets the map’s inline style to position:relative (among other things), against your will. So, any styling and positioning needs to be done on the wrapping element.

2. Putting a marker on the map

Now let’s get to the meat of the problem. For our markers, we do want to use a .service as a constructor for our OverlayView. According to the recommended way create an overlay view, we need to subclass the OverlayView class. As a general good practice, we try to avoid directly decorating and passing around outside vendor-owned objects. That’s why we will safely house the OverlayView in a service. When we create our custom marker object then, we can keep all our own customized methods and properties separate from the google-modified methods and properties.

Here’s what that service will look like:

.service(‘mapMarkerConstructor’, function () {
const GoogleOverlayView = function(element, latlng) {
this.element = element;
this.latlng = latlng;
};
GoogleOverlayView.prototype = new google.maps.OverlayView();
GoogleOverlayView.prototype.draw = function() {
let panes = this.getPanes();
let point = this.getProjection().fromLatLngToDivPixel(this.latlng);
panes.overlayImage.appendChild(this.element[0]);
if (point) {
this.element.css(‘left’, point.x + ‘px’);
this.element.css(‘top’, point.y + ‘px’);
}
};
GoogleOverlayView.prototype.onRemove = function() {};
this.GoogleOverlayView = GoogleOverlayView;
})

Couple things to point out here. We are setting our GoogleOverlayView.prototype to a new instance of a google.maps.OverlayView() – this gives us the secret sauce we need for fully customizable markers. Our .draw method does the work of translating our latitude/longitude coordinates to pixel coordinates on the map, and appending it in the correct div layer, or “pane”. Note that we are using “left” and “top” CSS pixel coordinates. This means whatever marker you have will need to be adjusted so it’s center lies at the given (x,y) instead of its top left corner.

We’ve also set up our GoogleOverlayView constructor to require just two parameters to build a marker: a marker DOM element, and the location of the marker expressed in lat/lng coordinates. Pretty straightforward.

Minor side note for all you ES6 arrow function fans: We are deliberately using regular function() syntax here because within our constructor we need dynamic scope to protect our this from the parent context. Otherwise it would not prototypically inherit the OverlayView methods required for our .drawfunction: .getPanes and .getProjection.

So, our constructor service creates a marker, but we still need to add the marker to the map. We’ll create a directive and a DOM element that will represent our marker. And in that directive’s controller, we’ll just need to inject our constructor service, call a new instance of it, and set the marker to our map with .setMap().

Updated HTML:

<figure ng-controller=“MapCtrl”>
<gmap></gmap>
<map-marker></map-marker>
</figure>

A little CSS so we can see it:

map-marker {
width: 10px;
height: 10px;
margin-top: –5px;
margin-left: –5px;
border-radius: 50%;
background: violetred;
position: absolute;
display: block;
}

Our map marker directive:

.directive(‘mapMarker’, function () {
return {
restrict: ‘E’,
controller: ‘MapMarkerCtrl’
};
})

Which references a controller:

.controller(‘MapMarkerCtrl’, function($scope, $element, mapMarkerConstructor){
const latlng = new google.maps.LatLng(34.075328, -118.330432);
const googleOverlayView = new mapMarkerConstructor.GoogleOverlayView($element, latlng);
googleOverlayView.setMap($scope.gmap);
})

Notice where $scope.gmap comes from. We originally set that in our parent map controller, so our child markers have access to it because their scopes are not isolated.

Notice also that in our HTML, <map-marker> is placed outside of, as a sibling of <gmap>. Why not inside of it? Because when google.maps.Map() creates a new map, it replaces all of the target element’s inner content. We are sort of forced to add the marker to the map only after it’s drawn. Not the prettiest pattern, but we’ll live.

At this point we should have a marker on our map, dropped somewhere in the Greater Wilshire/Hancock Park area of Los Angeles.

single custom map marker
The marker uses the same coordinates as the map center

3. Generating multiple markers

What happens when we want to show a bunch of markers at once? Presumably, we would have some data set of locations we would load to populate the map dynamically, each with their own set of properties. Let’s see that work by creating a new locationsModel factory for our ‘MapCtrl’ controller to ingest. You can imagine this factory being used to handle a response from some http request in the real world.

.factory(‘locationsModel’, function() {
const locationsModel = [{
name: ‘Wilshire Country Club’,
color: ‘darkblue’,
lat: 34.077796,
lng: -118.331151
},{
name: ‘301 N Rossmore Ave’,
color: ‘fuchsia’,
lat: 34.077146,
lng: -118.327805
},{
name: ‘5920 Beverly Blvd’,
color: ‘red’,
lat: 34.070281,
lng: -118.331831
}];
return locationsModel;
})

Inject locationsModel into our ‘MapCtrl’ controller, and make it available on scope:

.controller(‘MapCtrl’, function($scope, $element, locationsModel) {
const mapEl = $element.find(‘gmap’)[0]
const mapOptions = {
zoom: 14,
center: {lat: 34.075328,lng: -118.330432}
};
const gmap = new google.maps.Map(mapEl, mapOptions);
$scope.gmap = gmap;
$scope.locations = locationsModel;
})

And now, make our markers ng-repeat on that data, exposing each location object to the ‘MapMarkerCtrl’ controller scope. With that, we can pass in any of the location properties we want. Its lat/lng, and its color, with the help of ng-style.

<figure ng-controller=“MapCtrl”>
<gmap></gmap>
<map-marker
ng-repeat=“location in locations”
ng-style=“{‘background-color’:location.color}”>

</map-marker>
</figure>

colored map markers generated by data
Colored Markers!

Let’s add some life to these markers with event behaviors, and bind them to a hoverable list.

4. Adding marker events

Now the cool part. We have our locationsModel stored at the service level, to which we gave our ‘MapCtrl’ controller access. How would we generate an unordered list off this same data, and bind events between them and our markers? What we need is two-way binding, thankfully just what Angular does best! All we need to do is create a new controller and reference the same locationsModel.

.controller(‘LocationsCtrl’, function($scope, locationsModel){
$scope.locations = locationsModel;
})

Give it some HTML:

<ul ng-controller=“LocationsCtrl”>
<li ng-repeat=“location in locations”>
{{location.name}}
</li>
</ul>

And minimal styling:

ul {
position: relative;
left: 50%;
padding: 1em;
}
li {
font-family: sans-serif;
color: #999;
margin-bottom: .5em;
cursor: pointer;
}
li:hover{
color: #222;
}

Now, when we make modifications to the locationsModel from either the ‘LocationsCtrl’ or the ‘MapCtrl’, or its children, the ‘MapMarkerCtrl’ controllers, the change will be reflected everywhere.

Case in point: let’s add an .isActive property to each location in the locationsModel, so we can control the markers active state with CSS. For now, we’ll just enlarge the marker, and add a little animation transition pizzaz.

map-marker {
width: 10px;
height: 10px;
margin-top: –5px;
margin-left: –5px;
border-radius: 50%;
background-color: grey;
position: absolute;
display: block;
cursor: pointer;
transition: all .1s;
}
map-marker.active {
width: 20px;
height: 20px;
margin-top: –10px;
margin-left: –10px;
}

We’ll add it to the list items by binding it to ngMouseenter/ngMouseleave events:

<ul ng-controller=“LocationsCtrl”>
<li
ng-repeat=“location in locations”
ng-mouseenter=“location.isActive=true”
ng-mouseleave=“location.isActive=false”>

{{location.name}}
</li>
</ul>

And we’ll do the same thing to the markers themselves:

<figure ng-controller=“MapCtrl”>
<gmap></gmap>
<map-marker
ng-repeat=“location in locations”
ng-style=“{‘background-color’:location.color}”
ng-class=“{‘active’:location.isActive}”
ng-mouseenter=“location.isActive=true”
ng-mouseleave=“location.isActive=false”>

</map-marker>
</figure>

Thanks to two-way binding, that’s all the code we have to write. The .isActiveproperty now lives on each location in the locationsModel, and can be changed via hover events on both the markers and list items. The markers live!

hoverable google map markers bound to the data model
Hover behavior on the markers

From here, you can see how easily expandable this would be. Add your own click events, marker hover tooltips, popups, etc, with no restrictions on marker icon type or behavior.

Checkout the final jsfiddle and happy coding!