Introduction
Welcome back!
Last time, we created an application that integrated Google Maps directly into the MEAN stack. The app provided us a panel to create users, tag their location based on latitude and longitude, and validate their whereabouts using HTML5 geolocation.
As of this writing, over 150 users have added themselves to our demo map, with diverse locations strewn from San Francisco to Melbourne — which is already pretty cool when you think about it!
Today, we’ll be taking our work a step further by adding a new control panel that allows us to filter users based on a variety of fields. The final product will allow us to query our map based on gender, age, favorite language, proximity, and whether a user’s location has been HTML5 verified. Additionally, this tutorial will give us an opportunity to introduce some of MongoDB’s geospatial query tools.
As you follow along, feel encouraged to grab the source code. Also, if you’re joining us for the first time, you can download the code from Part I using this link.
Revised App Skeleton
To begin, let’s make some adjustments to our app’s structure. Go ahead and create a new queryCtrl.js
file as well as a directory called partials
, which will hold the files addForm.html
and queryForm.html
.
MapApp -- app // BACKEND ---- model.js ---- routes.js -- public // FRONTEND ---- index.html ---- js ------ app.js ------ addCtrl.js ------ queryCtrl.js // *new* ------ gservice.js ---- style.css ---- partials // *new* ------ addForm.html // *new* ------ queryForm.html // *new* -- server.js // EXPRESS SERVER -- package.json
Creating the Query View
Since our app wil now have two separate control panels — one for adding users and one for querying users, we’re going to utilize Angular’s routing module ngRoute to display the correct panel when needed.
To do this, we’re going to store the code associated with each panel in its own HTML partial. We’ll then specify in our main Angular module (app.js
) that our application should display the queryForm
partial when the URL includes /find
and the addForm
partial for all other URLs.
Let’s go ahead and extract the ‘Add Form’ code previously found in our index.html
file and paste it into the addForm.html
file of our partials
folder.
<!-- addForm.html --> <!-- "Join Team" (Post) Form --> <div class="col-md-5"> <!-- Creates Main Panel --> <div class="panel panel-default"> <!-- Panel Title --> <div class="panel-heading"> <h2 class="panel-title text-center">Join the Scotch Team! <span class="glyphicon glyphicon-map-marker"></span></h2> </div> <!-- Panel Body --> <div class="panel-body"> <!-- Creates Form (novalidate disables HTML validation, Angular will control) --> <form name ="addForm" novalidate> <!-- Text Boxes and Other User Inputs. Note ng-model binds the values to Angular $ scope --> <div class="form-group"> <label for="username">Username <span class="badge">All fields required</span></label> <input type="text" class="form-control" id="username" placeholder="OldandGold" ng-model="formData.username" required> </div> <label class="radio control-label">Gender</label> <div class="radio"> <label> <input type="radio" name="optionsRadios" id="radiomale" value="Male" ng-model="formData.gender"> Male </label> </div> <div class="radio" required> <label> <input type="radio" name="optionsRadios" id="radiofemale" value="Female" ng-model="formData.gender"> Female </label> </div> <div class="radio"> <label> <input type="radio" name="optionsRadios" id="radioother" value="What's it to ya?" ng-model="formData.gender"> What's it to ya? </label> </div> <div class="form-group"> <label for="age">Age</label> <input type="number" class="form-control" id="age" placeholder="72" ng-model="formData.age" required> </div> <div class="form-group"> <label for="language">Favorite Language</label> <input type="text" class="form-control" id="language" placeholder="Fortran" ng-model="formData.favlang" required> </div> <div class="form-group"> <label for="latitude">Latitude</label> <input type="text" class="form-control" id="latitude" value="39.500" ng-model="formData.latitude" readonly> </div> <div class="form-group"> <label for="longitude">Longitude</label> <input type="text" class="form-control" id="longitude" value="-98.350" ng-model="formData.longitude" readonly> </div> <div class="form-group"> <!-- Note RefreshLoc button tied to addCtrl. This requests a refresh of the HTML5 verified location. --> <label for="verified">HTML5 Verified Location? <span><button ng-click="refreshLoc()" class="btn btn-default btn-xs"><span class="glyphicon glyphicon-refresh"></span></button></span></label> <input type="text" class="form-control" id="verified" placeholder= "Nope (Thanks for spamming my map...)" ng-model="formData.htmlverified" readonly> </div> <!-- Submit button. Note that its tied to createUser() function from addCtrl. Also note ng-disabled logic which prevents early submits. --> <button type="submit" class="btn btn-danger btn-block" ng-click="createUser()" ng-disabled="addForm.$ invalid">Submit</button> </form> </div> </div> </div>
Next, let’s paste the code associated with our new Query Form into the queryForm.html
of the same folder.
<!-- queryForm.html --> <!-- Find Teammates (Query) Form --> <div class="col-md-5"> <!-- Creates Main Panel --> <div class="panel panel-default"> <!-- Panel Title --> <div class="panel-heading"> <h2 class="panel-title text-center">Find Teammates! (Map Query) <span class="glyphicon glyphicon-search"></span></h2> </div> <!-- Panel Body --> <div class="panel-body"> <!-- Creates Form --> <form name ="queryForm"> <!-- Text Boxes and Other User Inputs. Note ng-model binds the values to Angular $ scope --> <div class="form-group"> <label for="latitude">Your Latitude</label> <input type="text" class="form-control" id="latitude" placeholder="39.5" ng-model="formData.latitude" readonly> </div> <div class="form-group"> <label for="longitude">Your Longitude</label> <input type="text" class="form-control" id="longitude" placeholder="-98.35" ng-model="formData.longitude" readonly> </div> <div class="form-group"> <label for="distance">Max. Distance (miles)</label> <input type="text" class="form-control" id="distance" placeholder="500" ng-model="formData.distance"> </div> <!-- Note ng-true-value which translates check values into explicit gender strings --> <label>Gender</label> <div class="form-group"> <label class="checkbox-inline"> <input type="checkbox" name="optionsRadios" id="checkmale" value="Male" ng-model="formData.male" ng-true-value = "'Male'"> Male </label> <label class="checkbox-inline"> <input type="checkbox" name="optionsRadios" id="checkfemale" value="Female" ng-model="formData.female" ng-true-value="'Female'"> Female </label> <label class="checkbox-inline"> <input type="checkbox" name="optionsRadios" id="checkother" value="What's it to ya?" ng-model="formData.other" ng-true-value="'What\'s it to ya?'"> What's it to ya? </label> </div> <div class="form-group"> <label for="minage">Min. Age</label> <input type="number" class="form-control" id="minage" placeholder="5" ng-model="formData.minage"> </div> <div class="form-group"> <label for="maxage">Max Age</label> <input type="number" class="form-control" id="maxage" placeholder="80" ng-model="formData.maxage"> </div> <div class="form-group"> <label for="favlang">Favorite Language</label> <input type="text" class="form-control" id="favlang" placeholder="Fortran" ng-model="formData.favlang"> </div> <div class="form-group"> <div class="checkbox"> <label> <input type="checkbox" name="verified" id="radiomale" value="True" ng-model="formData.verified"> <strong>Include Only HTML5 Verified Locations?</strong> </label> </div> </div> <!-- Query button. Note that its tied to queryUsers() function from queryCtrl. --> <button type="submit" class="btn btn-danger btn-block" ng-click="queryUsers()">Search</button> </form> </div> <!-- Footer panel for displaying count. Note how it will only display if queryCount is greater than 0 --> <div ng-show="queryCount>0" class="panel-footer"> <p class="text-center">Hot Dang! We Found {{queryCount}} Teammates.</p> </div> </div> </div>
Lastly, let’s update our meanMapApp
module in the app.js
file to include the Angular ngRoute
module and specify the templateURL
associated with each URL route.
// app.js // Declares the initial angular module "meanMapApp". Module grabs other controllers and services. Note the use of ngRoute. var app = angular.module('meanMapApp', ['addCtrl', 'geolocation', 'gservice', 'ngRoute']) // Configures Angular routing -- showing the relevant view and controller when needed. .config(function($ routeProvider){ // Join Team Control Panel $ routeProvider.when('/join', { controller: 'addCtrl', templateUrl: 'partials/addForm.html', // Find Teammates Control Panel }).when('/find', { templateUrl: 'partials/queryForm.html', // All else forward to the Join Team Control Panel }).otherwise({redirectTo:'/join'}) });
For those less familiar with Angular’s ngRoute
module, what we’ve done here is made use of Angular’s routeProvider
service to identify the URL our users are looking at in the browser. Thus, when a user is looking at a URL with a given suffix, Angular knows which pre-defined controller and templateURL to use.
As you can see, in the example above, when a user is looking at /join
(or any URL other than /find
), Angular will employ the addCtrl
controller that we created in Part I and display the content from our addForm.html
file. Similarly when a user is looking at /find
, the user will be displayed the queryForm.html
content. (Once we create the queryCtrl
controller, we will specify this here as well.)
Now that we have our partials ready, let’s update our index.html
file.
<!-- index.html --> <!doctype html> <!-- Declares meanMapApp as the starting Angular module --> <html class="no-js" ng-app="meanMapApp"> <head> <meta charset="utf-8"> <title>Scotch MEAN Map</title> <meta name="description" content="An example demonstrating Google Map integration with MEAN Apps"> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- CSS --> <link rel="stylesheet" href="http://feeds.feedblitz.com/~/t/0/0/scotch_io/~https://scotch.io/bower_components/bootstrap/dist/css/bootstrap.css"/> <link rel="stylesheet" href="http://feeds.feedblitz.com/~/t/0/0/scotch_io/~style.css"/> <!-- Google Maps API --> <script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyDrn605l7RPadiwdzsOlRw9O28lxfYBJ6s"></script> <!-- Modernizr --> <script src="https://scotch.io/bower_components/modernizr/bin/modernizr"></script> <!-- JS Source --> <script src="https://scotch.io/bower_components/jquery/jquery.js"></script> <script src="https://scotch.io/bower_components/angular/angular.js"></script> <script src="https://scotch.io/bower_components/angular-route/angular-route.js"></script> <script src="https://scotch.io/bower_components/angularjs-geolocation/dist/angularjs-geolocation.min.js"></script> <!-- Angular Source --> <script src="js/app.js"></script> <script src="js/addCtrl.js"></script> <script src="js/gservice.js"></script> </head> <!-- Removed ng-controller. Now this will be handled in app.js --> <body> <div class="container"> <div class="header"> <ul class="nav nav-pills pull-right"> <!-- Links to the two menu views included --> <li active><a href="http://feeds.feedblitz.com/~/t/0/0/scotch_io/~https://scotch.io/#/join">Join the Team</a></li> <li disabled><a href="http://feeds.feedblitz.com/~/t/0/0/scotch_io/~https://scotch.io/#/find">Find Teammates</a></li> </ul> <h3 class="text-muted">The Scotch MEAN MapApp</h3> </div> <!-- Map and Side Panel --> <div class="row content"> <!-- Google Map --> <div class="col-md-7"> <div id="map"></div> </div> <!-- Side Panel -- Now Handled by ng-view --> <div ng-view></div> </div> <hr/> <!-- Footer --> <div class="footer"> <p class="text-center"><span class="glyphicon glyphicon-check"></span> Created by Ahmed Haque for Scotch IO - <a href="http://feeds.feedblitz.com/~/t/0/0/scotch_io/~https://scotch.io/">App Tutorial</a> | <a href="http://feeds.feedblitz.com/~/t/0/0/scotch_io/~https://github.com/afhaque/MeanMapAppV2.0">Github Repo</a></p> </div> </div> </body> </html>
Here we’ve removed the content associated with our addForm
side-panel and replaced it with a generic reference to ng-view
. Angular’s routeProvider
will automatically replace this with the correct HTML partial.
Additionally, we’ve removed reference to the addCtrl
controller that previously existed in our body tag. Once again, our routeprovider
will instruct the page to use the correct controller based on the URL.
With that, its time for our first test!
Crank up your mongod
instance and run node server.js
. If you then head to localhost:3000
you should see our familiar map from Part I. Click the ‘Find Teammates’ link to see our new Query Form basking in all its glory.
Creating the Query Logic and Express Routes
Now its time to create the backend code for handling queries. Open your routes.js
file and paste the following code beneath your app.post('users')
code.
// routes.js // app.post('users') code for creating users... // ... // Retrieves JSON records for all users who meet a certain set of query conditions app.post('/query/', function(req, res){ // Grab all of the query parameters from the body. var lat = req.body.latitude; var long = req.body.longitude; var distance = req.body.distance; // Opens a generic Mongoose Query. Depending on the post body we will... var query = User.find({}); // ...include filter by Max Distance (converting miles to meters) if(distance){ // Using MongoDB's geospatial querying features. (Note how coordinates are set [long, lat] query = query.where('location').near({ center: {type: 'Point', coordinates: [long, lat]}, // Converting meters to miles. Specifying spherical geometry (for globe) maxDistance: distance * 1609.34, spherical: true}); } // ... Other queries will go here ... // Execute Query and Return the Query Results query.exec(function(err, users){ if(err) res.send(err); // If no errors, respond with a JSON of all users that meet the criteria res.json(users); }); });
We’ve done a couple of key things here. So let’s break it down:
- First, we created a new
POST
request handler for URLs with the suffix/query
. This handler expects a JSON request body, which specifies three parameters: latitude, longitude, and distance. These parameters are then converted and stored as variables in the handler. - We then created a generic Mongoose Query using the Query Builder format. This format begins by establishing a generic
query
object equal to the unfiltered search of all users in our database. - If a distance is provided, the Query Builder will add a new search condition that filters for all users that fall within the distance provided of the query’s coordinates (latitude, longitude). Here we’re using the MongoDB search parameter $ near and its associated properties
maxDistance
andspherical
to specify the range we’re looking to cover. We’re multiplying the distance of our query body by 1609.34, because we want to take our users’ input (in miles) and convert it into the units MongoDB expects (in meters). Lastly, we’re specifying that the distance should be determined assuming a spherical surface. This is important, because we’ll be evaluating distances across the globe, as opposed to a flat Euclidean surface. - Finally, we used
query.exec
to instruct Mongoose to run the final query. If the query encounters no errors, it will provide a JSON output of all users who meet the criteria.
At this point, let’s test what we have. To do this, re-run your application using node server.js
and place a few dummy markers on your map. Place two markers near each other on one side of the map and two markers a sizeable distance away. Then position your marker next to the first two markers and note the associated latitude and longitude.
Now, open up Postman and create a raw JSON POST
request to your /query
URL. Specify the latitude and longitude that you just noted and set a distance of 100. Then send the request.
If all went well, your response body should list only the new nearby markers and exclude the distant ones.
Repeat your POST
request, but set the distance much farther. Try 1000 or 5000 instead.
If all went well, your response should list the remaining markers as well.
We’ll examine the precision capabilities of our query a bit later, but for now, let’s add the remaining filter conditions.
To do this, paste the following code over the POST request we just created.
// routes.js // Retrieves JSON records for all users who meet a certain set of query conditions app.post('/query/', function(req, res){ // Grab all of the query parameters from the body. var lat = req.body.latitude; var long = req.body.longitude; var distance = req.body.distance; var male = req.body.male; var female = req.body.female; var other = req.body.other; var minAge = req.body.minAge; var maxAge = req.body.maxAge; var favLang = req.body.favlang; var reqVerified = req.body.reqVerified; // Opens a generic Mongoose Query. Depending on the post body we will... var query = User.find({}); // ...include filter by Max Distance (converting miles to meters) if(distance){ // Using MongoDB's geospatial querying features. (Note how coordinates are set [long, lat] query = query.where('location').near({ center: {type: 'Point', coordinates: [long, lat]}, // Converting meters to miles. Specifying spherical geometry (for globe) maxDistance: distance * 1609.34, spherical: true}); } // ...include filter by Gender (all options) if(male || female || other){ query.or([{ 'gender': male }, { 'gender': female }, {'gender': other}]); } // ...include filter by Min Age if(minAge){ query = query.where('age').gte(minAge); } // ...include filter by Max Age if(maxAge){ query = query.where('age').lte(maxAge); } // ...include filter by Favorite Language if(favLang){ query = query.where('favlang').equals(favLang); } // ...include filter for HTML5 Verified Locations if(reqVerified){ query = query.where('htmlverified').equals("Yep (Thanks for giving us real data!)"); } // Execute Query and Return the Query Results query.exec(function(err, users){ if(err) res.send(err); // If no errors, respond with a JSON of all users that meet the criteria res.json(users); }); });
What we’ve done here is successively added conditions that check if our user has provided distance, gender, age, language, or HTML5 verified constraints to the POST
body. If any of these constraints exist, we’ll add the associated query condition to our Query Builder. Take note of this example as it really highlights the value of Mongoose’s Query Builder for complex queries.
Speaking of complex queries, let’s go ahead and test one now. To do this, create a set of mock users in various locations with assorted characteristics.
Here I’ve created a set of markers around Indianapolis.
Let’s say, I’m creating a coding school that targets girls between the ages of 20-30 years of age, within the city limits (150 miles). I can convert these parameters into POST
request fields as shown below.
Then when I run the query, I see only the filtered set of results.
Huzzah! IndyCodingSchool here we come.
Creating the Query Controller
Okay. That was great, but writing JSON requests manually can seriously suck. We need to build our UI capabilities ASAP!
To begin, let’s paste the following code in our queryCtrl.js
file.
// queryCtrl.js // Creates the addCtrl Module and Controller. Note that it depends on 'geolocation' and 'gservice' modules. var queryCtrl = angular.module('queryCtrl', ['geolocation', 'gservice']); queryCtrl.controller('queryCtrl', function($ scope, $ log, $ http, $ rootScope, geolocation, gservice){ // Initializes Variables // ---------------------------------------------------------------------------- $ scope.formData = {}; var queryBody = {}; // Functions // ---------------------------------------------------------------------------- // Get User's actual coordinates based on HTML5 at window load geolocation.getLocation().then(function(data){ coords = {lat:data.coords.latitude, long:data.coords.longitude}; // Set the latitude and longitude equal to the HTML5 coordinates $ scope.formData.longitude = parseFloat(coords.long).toFixed(3); $ scope.formData.latitude = parseFloat(coords.lat).toFixed(3); }); // Get coordinates based on mouse click. When a click event is detected.... $ rootScope.$ on("clicked", function(){ // Run the gservice functions associated with identifying coordinates $ scope.$ apply(function(){ $ scope.formData.latitude = parseFloat(gservice.clickLat).toFixed(3); $ scope.formData.longitude = parseFloat(gservice.clickLong).toFixed(3); }); }); // Take query parameters and incorporate into a JSON queryBody $ scope.queryUsers = function(){ // Assemble Query Body queryBody = { longitude: parseFloat($ scope.formData.longitude), latitude: parseFloat($ scope.formData.latitude), distance: parseFloat($ scope.formData.distance), male: $ scope.formData.male, female: $ scope.formData.female, other: $ scope.formData.other, minAge: $ scope.formData.minage, maxAge: $ scope.formData.maxage, favlang: $ scope.formData.favlang, reqVerified: $ scope.formData.verified }; // Post the queryBody to the /query POST route to retrieve the filtered results $ http.post('/query', queryBody) // Store the filtered results in queryResults .success(function(queryResults){ // Query Body and Result Logging console.log("QueryBody:"); console.log(queryBody); console.log("QueryResults:"); console.log(queryResults); // Count the number of records retrieved for the panel-footer $ scope.queryCount = queryResults.length; }) .error(function(queryResults){ console.log('Error ' + queryResults); }) }; });
What we’ve done here is very similar to the work we did in Part I. We created a new module and controller called queryCtrl
. This controller relies on $ scope
to pull all of the form data from our active queryForm.html
file. These elements are converted into variables, which are then used to directly create an http POST
request to the /query
URL whenever the $ scope.queryUsers
function (associated with our query button) is triggered. Additionally, as was the case with our addCtrl
controller, the queryCtrl
has code for identifying a user’s current location and for handling click capture.
Now that our controller is ready, let’s add a reference to queryCtrl
in our main Angular module in app.js
.
// app.js var app = angular.module('meanMapApp', ['addCtrl', 'queryCtrl', 'geolocation', 'gservice', 'ngRoute'])
We’ll also update our $ routeProvider
to utilize this new controller when a user is looking at the /find
URL.
// app.js // Find Teammates Control Panel }).when('/find', { controller: 'queryCtrl', templateUrl: 'partials/queryForm.html',
Finally, we’ll include a link to the queryCtrl.js
script in our index.html
file.
<script src="js/queryCtrl.js"></script>
Now that we’ve completed everything, let’s repeat the example from before. But this time, use the form itself to conduct the search.
Since we haven’t updated our map service, we won’t see changes on the map just yet. However, if we open up our Google Developers Console (ctrl+shift+i
) and navigate to the console, we should see both our queryBody
and the queryResults
displayed.
If all went well, the query results should match the results you saw earlier.
Aha. Found them again!
Modifying the Google Maps Service
Now that we’re successfully filtering users, its time to visualize our results on the map itself. To do this, we’re going to make a series of modifications to our googleMapService found in gservice.js
.
Go ahead and paste the following code over our pre-existing refresh googleMapService.refresh
function.
// gservice.js // Refresh the Map with new data. Takes three parameters (lat, long, and filtering results) googleMapService.refresh = function(latitude, longitude, filteredResults){ // Clears the holding array of locations locations = []; // Set the selected lat and long equal to the ones provided on the refresh() call selectedLat = latitude; selectedLong = longitude; // If filtered results are provided in the refresh() call... if (filteredResults){ // Then convert the filtered results into map points. locations = convertToMapPoints(filteredResults); // Then, initialize the map -- noting that a filter was used (to mark icons yellow) initialize(latitude, longitude, true); } // If no filter is provided in the refresh() call... else { // Perform an AJAX call to get all of the records in the db. $ http.get('/users').success(function(response){ // Then convert the results into map points locations = convertToMapPoints(response); // Then initialize the map -- noting that no filter was used. initialize(latitude, longitude, false); }).error(function(){}); } };
Here what we’ve done is introduced a new optional parameter filteredResults
to the refresh
function.
As you may recall, from Part I, the original purpose of the refresh
function was to pull information on all users in our database through a GET
request to /users
and to convert this data into Google Map markers. These markers were then used to populate our map, which was then displayed to users with their own location marked as well.
By adding in the filteredResults
parameter, we’re adapting the refresh
function for a second purpose. In cases, where we want to show only filtered results, we’re going to circumvent the $ http
GET
request, and instead directly send a JSON that includes only the filter-limited results. We’ll be able to generate these results using the POST
request to the /query
route that we just created.
Once the refresh
function receives these filtered results, it will pass the JSON to our convertToMapPoints
function and store the converted set of Google Map markers in our locations
array. We can then initialize our map as before, but this time only the filtered results will be shown.
Next, let’s make one more change to make things more obvious. Go ahead and paste the below code over the initialize
function.
// gservice.js // Initializes the map var initialize = function(latitude, longitude, filter) { // Uses the selected lat, long as starting point var myLatLng = {lat: selectedLat, lng: selectedLong}; // If map has not been created... if (!map){ // Create a new map and place in the index.html page var map = new google.maps.Map(document.getElementById('map'), { zoom: 3, center: myLatLng }); } // If a filter was used set the icons yellow, otherwise blue if(filter){ icon = "http://maps.google.com/mapfiles/ms/icons/yellow-dot.png"; } else{ icon = "http://maps.google.com/mapfiles/ms/icons/blue-dot.png"; } // Loop through each location in the array and place a marker locations.forEach(function(n, i){ var marker = new google.maps.Marker({ position: n.latlon, map: map, title: "Big Map", icon: icon, }); // For each marker created, add a listener that checks for clicks google.maps.event.addListener(marker, 'click', function(e){ // When clicked, open the selected marker's message currentSelectedMarker = n; n.message.open(map, marker); }); }); // Set initial location as a bouncing red marker var initialLocation = new google.maps.LatLng(latitude, longitude); var marker = new google.maps.Marker({ position: initialLocation, animation: google.maps.Animation.BOUNCE, map: map, icon: 'http://maps.google.com/mapfiles/ms/icons/red-dot.png' }); lastMarker = marker; // Function for moving to a selected location map.panTo(new google.maps.LatLng(latitude, longitude)); // Clicking on the Map moves the bouncing red marker google.maps.event.addListener(map, 'click', function(e){ var marker = new google.maps.Marker({ position: e.latLng, animation: google.maps.Animation.BOUNCE, map: map, icon: 'http://maps.google.com/mapfiles/ms/icons/red-dot.png' }); // When a new spot is selected, delete the old red bouncing marker if(lastMarker){ lastMarker.setMap(null); } // Create a new red bouncing marker and move to it lastMarker = marker; map.panTo(marker.position); // Update Broadcasted Variable (lets the panels know to change their lat, long values) googleMapService.clickLat = marker.getPosition().lat(); googleMapService.clickLong = marker.getPosition().lng(); $ rootScope.$ broadcast("clicked"); }); };
The change here is minimal but visually significant. Here we’ve noted that if the initialize
function is called with the boolean filter
set to true, all markers should be yellow dots as opposed to the blue dots we’d been using before. This is helpful from a UI perspective, because it let’s users immediately realize that their query worked successfully.
Now that our refresh
function has been updated. Let’s finally update our queryCtrl
file to utilize this new service. Add the below line in place of the console.log
lines we used earlier.
// queryCtrl.js // $ http.post('/query, queryBody).successs(function(queryResults){... // Old console.log code ... // Pass the filtered results to the Google Map Service and refresh the map gservice.refresh(queryBody.latitude, queryBody.longitude, queryResults);
Here you can see that we’re taking the queryResults from our /query
POST
and directly sending the filtered results to our refresh
function for map building. And with this final step, let’s run one final test of our app!
Boot up your server.js
file and rerun the advanced query example we’ve run before using the Query Form. If all went well you should see prominent yellow markers, indicating the filtered results.
Victory. I see you!
Final Tweaks
There are plenty of ways to improve this app. As a few suggestions, I’d suggest reading up on GeoJSON, the various Google Map Options, and the different styling options available from Snazzy Maps. Additionally, there is significant momentum behind the Angular-Google-Maps Project, something definitely worth looking into if you’re planning to build more complex map applications.
That said, even with as simple a map application as this one — it’s remarkable how quickly you can draw meaningful information. Already our demo app has had close to 100 verified users sign up. Scanning through the locations, it’s been awesome to see the diversity of locations that Scotch readers login from. (Who knew Scotch had a follower in Bishkek, Kyrgzstan?).
Good luck with your own map making adventures! If you come up with something cool, definitely post about it in the comments.
We’d love to hear about it!
BONUS: “But just how accurate is this Mongo $ near thing…?”
Well. It turns out pretty damn accurate is the answer. While writing this tutorial, I’d stumbled into a few articles that anecdotally estimated that the $ near
function was good enough at discriminating distances within 5 miles — so I decided to test and see for myself. I ran a few experiments with coordinates a known distance away from each other, and looked for the minimum distance I could use to discriminate locations.
For small distances (where Euclidean “flat” geometry takes hold), Mongo distance queries were right on the money — consistently able to discriminate distances within 1 mile of the actual distance.
For larger distances (where more complex “spherical” geometries become relevant), the MongoDB distance queries were off by about ~10 miles. Not as good, but not shabby at all — especially, considering there exist multiple methods for calculating spherical distances.
All in all, this presents even more reason to play around with the Geospatial tools in MongoDB — so go forth with confidence young cartographers!