https://www.endpointdev.com/blog/tags/maps/2022-04-02T00:00:00+00:00End Point DevOn Shapefiles and PostGIShttps://www.endpointdev.com/blog/2022/04/shapefiles-postgis/2022-04-02T00:00:00+00:00Josh Tolley
<p><img src="/blog/2022/04/shapefiles-postgis/endurance-clip.webp" alt="Partial map of the voyage of the Endurance, from the book “South”, Ernest H. Shackleton">
Partial map of the voyage of the Endurance, from <a href="https://www.gutenberg.org/ebooks/5199">“South”, by Ernest Shackleton</a></p>
<p>The shapefile format is commonly used in geospatial vector data interchange, but as it’s managed by a commercial entity, Esri, and as GIS is a fairly specialized field, and perhaps because the format specification is only <a href="https://en.wikipedia.org/wiki/Shapefile">“mostly open”</a>, these files can sometimes be confusing to the newcomer. Perhaps these notes can help clarify things.</p>
<p>Though the name “shapefile” would suggest a single file in filesystem parlance, a shapefile requires at least three different files, including filename extensions <code>.shp</code>, <code>.shx</code>, and <code>.dbf</code>, stored in the same directory, and the term “shapefile” often refers to that directory, or to an archive such as a zipfile or tarball containing that directory.</p>
<h3 id="qgis">QGIS</h3>
<p><a href="https://qgis.org">QGIS</a> is an open-source package to create, view, and process GIS data. One good first step with any shapefile, or indeed any GIS data, is often to take a look at it. Simply tell QGIS to open the shapefile directory. It may help to add other layers, such as one of the world map layers QGIS provides by default, to see the shapefile data in context.</p>
<h3 id="gdal">GDAL</h3>
<p>Though QGIS can convert between GIS formats itself, I prefer working in a command-line environment. <a href="https://gdal.org/">The GDAL software suite</a> aims to translate GIS data between many available formats, including shapefiles. I most commonly use its <code>ogr2ogr</code> command-line utility, along with the excellent accompanying manpage.</p>
<p>In short, a typical <code>ogr2ogr</code> command tells the utility where to find the input data and where to put the converted output, optionally with various reformatting and processing options. You’ll find some examples below.</p>
<h3 id="postgis">PostGIS</h3>
<p>Much of our (ok, my) GIS work has involved <a href="https://postgis.net">PostGIS</a>, an extension to the PostgreSQL database for handling GIS data. It’s been convenient for me to process GIS data using the same language and tools I use to process other data. It uses GDAL’s libraries internally.</p>
<h3 id="examples">Examples</h3>
<h4 id="import-shapefile-data-into-postgis">Import Shapefile data into PostGIS</h4>
<p>The example below comes from a customer’s project we recently worked on. They provided us a set of several shapefiles, which I first arranged in a directory structure. This code imports each of them into a PostGIS database, in the <code>shapefiles</code> schema.</p>
<p>The other arguments to <code>ogr2ogr</code> specify the output format (“PostgreSQL”), the destination database name, and the directory which stores the shapefile. <code>ogr2ogr</code> expects the destination and source arguments in that order, as two positional arguments, so here the destination is <code>PG:dbname=destdb</code>, and the source file name comes from the the <code>$i</code> script variable.</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#080;font-weight:bold">for</span> i in <span style="color:#d20;background-color:#fff0f0">`</span>find . -name <span style="color:#d20;background-color:#fff0f0">"*shp"</span><span style="color:#d20;background-color:#fff0f0">`</span>; <span style="color:#080;font-weight:bold">do</span>
<span style="color:#369">j</span>=<span style="color:#080;font-weight:bold">$(</span>basename <span style="color:#369">$i</span><span style="color:#080;font-weight:bold">)</span>
<span style="color:#369">k</span>=<span style="color:#33b;background-color:#fff0f0">${</span><span style="color:#369">j</span>/.shp/<span style="color:#33b;background-color:#fff0f0">}</span>
ogr2ogr -f PostgreSQL -nln shapefiles.<span style="color:#33b;background-color:#fff0f0">${</span><span style="color:#369">k</span><span style="color:#33b;background-color:#fff0f0">}</span> PG:dbname=destdb <span style="color:#369">$i</span>
<span style="color:#080;font-weight:bold">done</span>
</code></pre></div><h4 id="export-postgis-data-as-kml">Export PostGIS data as KML</h4>
<p>This example creates a KML file from PostGIS query results. The arguments provide the query to use to fetch the data, the output format (“KML”), the output file name, and the source database. This will create a KML file containing a set of unstyled placemarks, with names from the <code>property_code</code> column, and geometry data from the <code>outline_geom</code> column in the <code>properties</code> table of our database.</p>
<p>In this project, <code>outline_geom</code> contained GIS “linestrings”, data types consisting of a series of lines, which <code>ogr2ogr</code> translated into KML polygons. Had <code>outline_geom</code> contained points, for instance, the KML result would also have been points. In other words, <code>ogr2ogr</code> automatically chooses the correct KML object type based on the GIS object type in the input data.</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">ogr2ogr -sql <span style="color:#d20;background-color:#fff0f0">"select property_code, outline_geom from properties"</span> -f KML outlines.kml PG:dbname=properties
</code></pre></div><p>Note that though the examples above use PostGIS, <code>ogr2ogr</code> can take shapefile input and produce KML output directly without the PostGIS intermediary. We used PostGIS in these cases for other purposes, such as to filter the output and limit the attributes stored in the KML result.</p>
<p>By default, <code>ogr2ogr</code> puts all the attributes from the shapefile into <code>ExtendedData</code> elements in the KML, but in our case we didn’t want those. We also didn’t want all the entries in the shapefile in our resulting KML. To skip the PostGIS step, we might do something like this:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">ogr2ogr -f kml output.kml shapefile_directory/
</code></pre></div><p>What tools do you use for shapefile processing? Please let us know!</p>
Campendium: A Responsive, Fancy Detail Pagehttps://www.endpointdev.com/blog/2019/09/campendium-detail-page/2019-09-09T00:00:00+00:00Steph Skardal
<p><img src="/blog/2019/09/campendium-detail-page/banner.jpg" alt=""></p>
<p>This week, I was very excited to deploy a project for <a href="https://www.campendium.com/">Campendium</a>, one of our long-time clients. As noted in <a href="/blog/2019/08/campendium-updates/">my recent post on Campendium updates for the year</a>, Campendium has thousands of listings of places to camp and provides a great infrastructure for the development of a rich community of travelers around North America.</p>
<p>For the past few months, I’ve been working on a significant update to Campendium’s campground detail page, the page template where in-depth information is provided for each of Campendium’s locations. This is equivalent to a product detail page on an ecommerce site.</p>
<p>The “guts” of the update included a new detail page design with expanded responsiveness, the introduction of 360° videos, and expanded user driven content in the form of Question & Answer (Q&A or QnA), reviews, notes, nightly rates, etc. Read on to find out more and see video examples of several of the features!</p>
<h3 id="user-interface--responsiveness-updates">User Interface & Responsiveness Updates</h3>
<p>One of the things the Campendium team and I are most proud of here is the responsiveness of the new design. In the case of traveling and camping, responsiveness is important since a large amount of traffic comes from mobile devices, relative to what you might see in other industries.</p>
<p>User images are shown as “hero images” and the user interface updates depending on the browser device and width, as shown in the following videos for <a href="https://www.campendium.com/gilbert-ray-campground">Gilbert Ray Campground</a> and <a href="https://www.campendium.com/cayuga-lake-state-park">Cayuga Lake State Park</a>.</p>
<p style="text-align:center;font-weight:bold;"><iframe src="https://drive.google.com/file/d/1FFF8GcztnV-KGaJ4pjDYixCbkxw6kduu/preview" width="770" height="450"></iframe>A preview of responsive behavior on the campground detail page with user submitted photos.</p>
<p style="text-align:center;font-weight:bold;"><iframe src="https://drive.google.com/file/d/1caQZT9ogh_piuTX2UDIvWGEBZse0zwnx/preview" width="770" height="450"></iframe>A second preview of responsive behavior on the campground detail page with embedded maps.</p>
<p>Another updated design usability tweak was a sticky navigation bar to navigation throughout the page, which can get especially long with user submitted content. See how the “Overview”, “Video”, etc. links become sticky as you scroll down on the page, and the current region coming into view of the page is underlined:</p>
<p style="text-align:center;font-weight:bold;"><iframe src="https://drive.google.com/file/d/1w_MjP_HUXntYXHTG6FeREtMWwhKv4KgG/preview" width="770" height="450"></iframe>Navigation becomes fixed to the top of the browser as a user scrolls through the content.</p>
<p>Campendium uses Ruby on Rails as a backend, a bit of <a href="https://getbootstrap.com/">Bootstrap</a>, and <a href="https://sass-lang.com/">Sass</a> and best practice responsive design is used throughout this updated user interface.</p>
<h3 id="360-videos">360° Videos</h3>
<p>The Campendium team has been hard at work creating 360 degree videos of the various locations to provide significant value to their users. These videos are now embedded into the campground detail page. Video hosting is provided by YouTube and dropped into the Campendium campground HTML template.</p>
<p style="text-align:center;font-weight:bold;"><iframe src="https://drive.google.com/file/d/12XEu-95diRRMjlqb28jD0xdj9qIWXSb7/preview" width="770" height="450"></iframe>A video in a video—a 360 degree video example.</p>
<p>I personally think these 360° videos are awesome, especially to those visual learners out there! You can learn so much from a video that can be hard to capture in user reviews.</p>
<h3 id="community-qa">Community Q&A</h3>
<p>Another new feature with this deploy is the introduction of community driven Question & Answer. Users can submit questions about a campground, and users who have contributed to this campground are invited to respond (or they can opt-out site-wide to responding to questions). In addition to Q&A itself, user content can be “upvoted” (marked as Helpful) or flagged for an improved user content browsing experience.</p>
<p><img src="/blog/2019/09/campendium-detail-page/community.png" alt="Campendium community in Question & Answer"></p>
<p>All Q&A functionality is driven by JavaScript coupled with the Ruby on Rails backend.</p>
<h3 id="expansion-of-user-contributed-content-and-features">Expansion of User Contributed Content and Features</h3>
<p>Finally, this update includes expansion of user contributed content in addition to Q&A. This includes the ability for users to contribute notes, cell signal reports, and nightly rates. Reviews can now be filtered by user profile information and sorted by date or specific ranking. Reviews can also be searched by keywords and those keywords highlighted in the results. The following video demonstrates sorting and searching of reviews with highlighted keyword match:</p>
<p style="text-align:center;font-weight:bold;"><iframe src="https://drive.google.com/file/d/1G2KGfxCkOmuJm-AJE8BPXUC-OWdw1iQS/preview" width="770" height="450"></iframe>Review searchability with highlighting and filterability.</p>
<p>All search functionality driven by JavaScript on the frontend, and a Ruby on Rails backend coupled with <a href="https://github.com/sunspot/sunspot">Sunspot</a> and <a href="https://lucene.apache.org/solr/">Solr</a>.</p>
<h3 id="who-doesnt-love-stats">Who Doesn’t Love Stats?</h3>
<p>Just for the sake of sharing stats, from GitHub, this update included:</p>
<ul>
<li>215 changed files</li>
<li>6,284 additions</li>
<li>3,924 deletions (removal / cleanup of unneeded JavaScript files)</li>
</ul>
Campendium v2019: A Summary of Recent Updateshttps://www.endpointdev.com/blog/2019/08/campendium-updates/2019-08-05T00:00:00+00:00Steph Skardal
<p>This year has brought a handful of exciting changes for <a href="https://www.campendium.com/">Campendium</a>, one of End Point’s long-time clients, by yours truly. Created by campers for campers, Campendium has thousands of listings of places to camp, from swanky RV parks to free remote destinations, vetted by a team of full-time travelers and reviewed by over 200,000 members. I thought I would take some time to summarize these recent updates.</p>
<h3 id="maps-and-clustering">Maps and Clustering</h3>
<p><img src="/blog/2019/08/campendium-updates/map-clusters.png" alt="Campendium map clustering of campground locations"></p>
<p>Campendium uses <a href="https://www.mapbox.com/">Mapbox</a> for map rendering to display campgrounds and locations throughout North America. One of the new features added this year was <a href="https://docs.mapbox.com/mapbox-gl-js/example/cluster/">clustering</a> of campground locations, where campgrounds are grouped together and presented in a “cluster” with a size relative to how many campgrounds are in the cluster.</p>
<p>If a user is searching for campgrounds in a broad location, they can see where campgrounds might be more densely grouped by location. Once a user zooms in zoom in a couple of clicks, the campgrounds are no longer clustered and individual campgrounds locations can be seen. While working on this update, we spent a good amount of our time tweaking and troubleshooting the optimal clustering behavior to provide the most benefit to those searching for a campground. <a href="https://docs.mapbox.com/mapbox-gl-js/api/">Mapbox GL JS</a> works in parallel with <a href="https://reactjs.org/">ReactJS</a>, and runs with a Ruby on Rails back-end.</p>
<p><img src="/blog/2019/08/campendium-updates/map-non-clusters.jpg" alt="Campendium map non-clustering of campground locations after zooming in"></p>
<h3 id="advanced-filtering">Advanced Filtering</h3>
<p><img src="/blog/2019/08/campendium-updates/map-filtering.jpg" alt="Campendium advanced filtering"></p>
<p>Another exciting was the introduction of advanced filtering in the search interface, presented in combination with map display. Users can filter campgrounds by category (e.g. Public Land, RV Parking, Parking, Dump Station), filter by price (with a slider), hookups, campground policy (e.g. age or pet restrictions), discounts, recreation, and facilities. All of this search filtering is driven by <a href="https://github.com/sunspot/sunspot">Sunspot</a>, a Ruby on Rails gem for working with the popular <a href="https://lucene.apache.org/solr/">Solr</a> search engine. Results can be sorted by user provided reviews, price or distance from a specific GPS location. Here, much care was given to provide the best user interface for presenting this valuable functionality.</p>
<h3 id="supporters-only-features">“Supporters Only” Features</h3>
<p><img src="/blog/2019/08/campendium-updates/supporters.jpg" alt="Campendium Supporters only, Subscriptions"></p>
<p>Another recent update to Campendium includes functionality to offer user subscriptions. Registered users can sign up to support Campendium on a monthly or annual basis, and subscriptions are set to auto-renew at the end of their subscription period. This paid support hides advertisements throughout the site (advertisements are controlled by a third party), and advanced filtering on cell reception. There are plans to expand supporter features in the future. Ruby on Rails combined with <a href="https://stripe.com/docs/stripe-js/reference">StripeJS</a> is used to manage subscription payments, and Ruby on Rails also serves as a backend for <a href="https://developer.apple.com/in-app-purchase/">In-App Purchases</a> of subscriptions from the App store.</p>
<h3 id="always-responsive-and-latency-aware">Always Responsive and Latency-Aware</h3>
<p>Because a large portion of the Campendium visitors are on the road, it’s important to have both a responsive design and to build for bandwidth limitations for users. Throughout the development of these new features, responsive and mobile friendly designs were implemented leveraging <a href="https://sass-lang.com/">Sass</a>, sometimes requiring help from the rest of the knowledgeable <a href="/team/">End Point team</a>!</p>
<p>Many of the pages throughout the site are fully cached including the homepage, search result pages, and campground detail page, and cookies are used to indicate user status. In some cases, user submitted campground images are lazy-loaded to mitigate bandwidth limitations.</p>
<p><img src="/blog/2019/08/campendium-updates/mobile.jpg" alt="Mobile, responsive design for Campendium"></p>
<h3 id="whats-next">What’s Next?</h3>
<p>While I didn’t go into much technical depth on these updates, I am happy that the updates represent a broad spectrum of full-stack development skills featuring nginx, Ruby on Rails, 3rd party integration including StripeJS, MapboxGL and IAP (Apple), a JavaScript framework with ReactJS, and working with Ruby gems to leverage other tools, for example, Solr (Sunspot) and Sass.</p>
<p>In the future, Campendium plans to continue using these tools to see a more interactive, social campground detail page, and has plans to expand outside of North America. You can visit Campendium <a href="https://www.campendium.com/">here</a>, or find them on Instagram <a href="https://www.instagram.com/campendium/?hl=en">here</a> to follow their exciting announcements!</p>
Switching from Google Maps to Leaflethttps://www.endpointdev.com/blog/2019/03/switching-google-maps-leaflet/2019-03-23T00:00:00+00:00Juan Pablo Ventoso
<p><img src="/blog/2019/03/switching-google-maps-leaflet/leaflet-weather-map-us.jpg" alt="Leaflet Weather map example" /><br>Photo: <a href="https://www.extendedforecast.net/radsat">RadSat HD</a></p>
<p>It’s no news for anyone who has Google Maps running on their websites that Google started charging for using their API. We saw it coming when, back in 2016, they started requiring a key to add a map using their JavaScript API. And on June 11, 2018, they did a major upgrade to their API and billing system.</p>
<p><b>The consequence?</b> Any website with more than 25,000 page loads per day will have to pay. And if you are using a dynamic map (a map with custom styling and/or content) you only have roughly 28,000 free monthly page loads. We must create a billing account, <em>even if we have a small website with a couple of daily visitors</em>, hand credit card information to Google, and monitor our stats to make sure we won’t be charged. And if we don’t do that, our map will be dark and will have a “For development only” message in the background.</p>
<p>So what are your options? You can either pay or completely remove Google Maps from your websites. Even enterprise weather websites like <a href="https://weather.com/weather/radar/interactive/l/USNY0996:1:US">The Weather Channel</a> or <a href="https://www.wunderground.com/wundermap">Weather Underground</a> have now replaced their Google Maps API calls with an alternative like Leaflet or MapBox (in some cases, they even gained some functionality in the process).</p>
<p>I have a <a href="https://www.extendedforecast.net">personal weather website</a>, and when I heard big changes were coming, I started to move away from Google Maps as well. My choice at that moment was Leaflet: It has everything you may need to build a robust tile-based map, add layers, markers, animations, custom tiles, etc. And it’s BSD-licensed <b>open source and free</b>.</p>
<h3 id="creating-a-basic-map">Creating a basic map</h3>
<p><img src="/blog/2019/03/switching-google-maps-leaflet/google-vs-leaflet-look-and-feel.jpg" /><br><small>Google Map conversion to Leaflet can be almost seamless if the same tiles are used.</small></p>
<p>Google Maps API and Leaflet share a similar way of doing most things, but they have some key differences we need to take into account. As a general rule, Google used the “google.maps” prefix to name most classes and interfaces, while Leaflet uses the “L” prefix instead.</p>
<p>First thing we need to do is to remove the Google Maps API reference from our website(s). So we need to replace the reference:</p>
<pre tabindex="0"><code><script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?key=[your_api_key]"></script>
</code></pre><p>With the references to the Leaflet map JavaScript and stylesheet URIs.</p>
<pre tabindex="0"><code><script src="https://unpkg.com/leaflet@1.0.2/dist/leaflet.js"></script>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.0.2/dist/leaflet.css" />
</code></pre><p>Now let’s take a look at the code needed to create a Google Map vs. a Leaflet map.</p>
<ul>
<li>Google:</li>
</ul>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-javascript" data-lang="javascript"><span style="color:#080;font-weight:bold">var</span> map = <span style="color:#080;font-weight:bold">new</span> google.maps.Map(<span style="color:#038">document</span>.getElementById(<span style="color:#d20;background-color:#fff0f0">"map"</span>), {
center: <span style="color:#080;font-weight:bold">new</span> google.maps.LatLng(<span style="color:#00d;font-weight:bold">40.7401</span>, -<span style="color:#00d;font-weight:bold">73.9891</span>),
zoom: <span style="color:#00d;font-weight:bold">12</span>,
mapTypeId: google.maps.MapTypeId.ROADMAP
});
</code></pre></div><p>Leaflet:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-javascript" data-lang="javascript"><span style="color:#080;font-weight:bold">var</span> map = <span style="color:#080;font-weight:bold">new</span> L.Map(<span style="color:#d20;background-color:#fff0f0">"map"</span>, {
center: <span style="color:#080;font-weight:bold">new</span> L.LatLng(<span style="color:#00d;font-weight:bold">40.7401</span>, -<span style="color:#00d;font-weight:bold">73.9891</span>),
zoom: <span style="color:#00d;font-weight:bold">12</span>,
layers: <span style="color:#080;font-weight:bold">new</span> L.TileLayer(<span style="color:#d20;background-color:#fff0f0">"https://tile.openstreetmap.org/{z}/{x}/{y}.png"</span>)
});
</code></pre></div><p>Quite similar, isn’t it? The main difference is that, in Leaflet, we need to provide a tile layer for the base map because there isn’t one by default. There are a lot of excellent tile layers available to use at no cost. Here are some of them:</p>
<ul>
<li><b>Bright</b>: <code>https://a.tiles.mapbox.com/v3/mapbox.world-bright/{z}/{x}/{y}.png</code></li>
<li><b>Topographic</b>: <code>https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png</code></li>
<li><b>Black and white</b>: <code>https://stamen-tiles-{s}.a.ssl.fastly.net/toner/{z}/{x}/{y}{r}.png</code></li>
</ul>
<p>You can browse other free tile layer providers for Leaflet on <a href="https://leaflet-extras.github.io/leaflet-providers/preview/">this link</a>. And of course, if you want to pay there’s a lot of affordable paid tiles out there too.</p>
<h3 id="adding-a-marker">Adding a marker</h3>
<p>Adding a marker is quite straightforward as well. It even looks easier on Leaflet than Google.</p>
<p>Google:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-javascript" data-lang="javascript"><span style="color:#080;font-weight:bold">var</span> marker = <span style="color:#080;font-weight:bold">new</span> google.maps.Marker({
position: <span style="color:#080;font-weight:bold">new</span> google.maps.LatLng(<span style="color:#00d;font-weight:bold">40.7401</span>, -<span style="color:#00d;font-weight:bold">73.9891</span>),
map: map,
title: <span style="color:#d20;background-color:#fff0f0">"End Point Corporation"</span>
});
</code></pre></div><p>Leaflet:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-javascript" data-lang="javascript"><span style="color:#080;font-weight:bold">var</span> marker = <span style="color:#080;font-weight:bold">new</span> L.Marker(<span style="color:#080;font-weight:bold">new</span> L.LatLng(<span style="color:#00d;font-weight:bold">40.7401</span>, -<span style="color:#00d;font-weight:bold">73.9891</span>));
marker.bindPopup(<span style="color:#d20;background-color:#fff0f0">"End Point Corporation"</span>);
map.addLayer(marker);
</code></pre></div><p>And that’s it: we have a working Leaflet map with a marker that displays a text when we click on it.</p>
<p><img src="/blog/2019/03/switching-google-maps-leaflet/leaflet-example-working.jpg" /><br><small>Screenshot of the Leaflet example. Code below, if you want to try it live:</small></p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-html" data-lang="html"><<span style="color:#b06;font-weight:bold">head</span>>
<<span style="color:#b06;font-weight:bold">title</span>>Leaflet map example — End Point Corporation</<span style="color:#b06;font-weight:bold">title</span>>
<<span style="color:#b06;font-weight:bold">script</span> <span style="color:#369">src</span>=<span style="color:#d20;background-color:#fff0f0">"https://unpkg.com/leaflet@1.0.2/dist/leaflet.js"</span>></<span style="color:#b06;font-weight:bold">script</span>>
<<span style="color:#b06;font-weight:bold">link</span> <span style="color:#369">rel</span>=<span style="color:#d20;background-color:#fff0f0">"stylesheet"</span> <span style="color:#369">href</span>=<span style="color:#d20;background-color:#fff0f0">"https://unpkg.com/leaflet@1.0.2/dist/leaflet.css"</span> />
</<span style="color:#b06;font-weight:bold">head</span>>
<<span style="color:#b06;font-weight:bold">body</span>>
<<span style="color:#b06;font-weight:bold">style</span>>
<span style="color:#b06;font-weight:bold">body</span> { <span style="color:#080;font-weight:bold">margin</span>: <span style="color:#00d;font-weight:bold">0</span> };
#<span style="color:#b06;font-weight:bold">map</span> { <span style="color:#080;font-weight:bold">height</span>: <span style="color:#00d;font-weight:bold">100</span><span style="color:#888;font-weight:bold">%</span> };
</<span style="color:#b06;font-weight:bold">style</span>>
<<span style="color:#b06;font-weight:bold">div</span> <span style="color:#369">id</span>=<span style="color:#d20;background-color:#fff0f0">"map"</span>></<span style="color:#b06;font-weight:bold">div</span>>
<<span style="color:#b06;font-weight:bold">script</span> <span style="color:#369">type</span>=<span style="color:#d20;background-color:#fff0f0">"text/javascript"</span>>
<span style="color:#080;font-weight:bold">var</span> endPointLocation = <span style="color:#080;font-weight:bold">new</span> L.LatLng(<span style="color:#00d;font-weight:bold">40.7401</span>, -<span style="color:#00d;font-weight:bold">73.9891</span>);
<span style="color:#080;font-weight:bold">var</span> map = <span style="color:#080;font-weight:bold">new</span> L.Map(<span style="color:#d20;background-color:#fff0f0">"map"</span>, {
center: endPointLocation,
zoom: <span style="color:#00d;font-weight:bold">12</span>,
layers: <span style="color:#080;font-weight:bold">new</span> L.TileLayer(<span style="color:#d20;background-color:#fff0f0">"https://tile.openstreetmap.org/{z}/{x}/{y}.png"</span>)
});
<span style="color:#080;font-weight:bold">var</span> marker = <span style="color:#080;font-weight:bold">new</span> L.Marker(endPointLocation);
marker.bindPopup(<span style="color:#d20;background-color:#fff0f0">"End Point Corporation"</span>);
map.addLayer(marker);
</<span style="color:#b06;font-weight:bold">script</span>>
</<span style="color:#b06;font-weight:bold">body</span>>
</code></pre></div><h3 id="layers-and-controls">Layers and controls</h3>
<p>From this point, we can start doing more complex things if we need to:</p>
<ul>
<li><b>Display images on the map</b>: <a href="https://leafletjs.com/reference-1.4.0.html#imageoverlay">ImageOverlay</a>.</li>
<li><b>Display a custom tile layer</b>: <a href="https://leafletjs.com/reference-1.4.0.html#tilelayer">TileLayer</a>.</li>
<li><b>Draw polygons, rectangles, circles</b>: <a href="https://leafletjs.com/reference-1.4.0.html#polygon">Polygon</a> - <a href="https://leafletjs.com/reference-1.4.0.html#rectangle">Rectangle</a> - <a href="https://leafletjs.com/reference-1.4.0.html#circle">Circle</a>.</li>
<li><b>Display GeoJSON data on the map</b>: <a href="https://leafletjs.com/reference-1.4.0.html#geojson">GeoJSON</a>.</li>
</ul>
<p>You can browse the <a href="https://leafletjs.com/reference-1.4.0.html">Leaflet API reference</a> for further details.</p>
<h3 id="plugins-and-tools">Plugins and tools</h3>
<p>There is some extended functionality in Google Maps that is not available in Leaflet by default unless we use a plugin. For example, if we want to add the “fullscreen” button to the top right corner, just as Google has it, or if we want to let the user draw polygons on top of the map, we’ll need to download and add the reference to the required plugins. Here is a list of the ones I’ve already used:</p>
<ul>
<li><b>“Fullscreen” button plugin</b>: <a href="https://github.com/Leaflet/Leaflet.fullscreen">Leaflet.fullscreen</a>.</li>
<li><b>Vector drawing and editing plugin</b>: <a href="https://github.com/Leaflet/Leaflet.draw">Leaflet.draw</a>.</li>
<li><b>Heatmap plugin</b>: <a href="https://github.com/Leaflet/Leaflet.heat">Leaflet.heat</a>.</li>
</ul>
<p>You can find more plugins at the <a href="https://github.com/Leaflet/">Leaflet GitHub account</a>. And of course, you can (and should!) contribute to improve them.</p>
<p>There is also some alternatives to additional services offered by Google like geocoding or routing. They might have some limitations involved, so it would be wise to take a look at their usage policies first.</p>
<ul>
<li><b>Geocoding API</b>: <a href="https://wiki.openstreetmap.org/wiki/Nominatim">Nominatim</a>.</li>
<li><b>Routing</b>: <a href="http://project-osrm.org/">Project ORSM</a> (free version has limited use).</li>
</ul>
<p>More services can be found at <a href="https://switch2osm.org/other-uses/">switch2osm.org/other-uses</a>.</p>
<h3 id="putting-it-all-together">Putting it all together</h3>
<p>I’ve been using Leaflet for almost a year now in an interactive weather map originally made with the Google Maps API. Of course, I’ve had some minor hiccups along the way, but having full control of the source code and resources allows you to add functionality, fix things or even rewrite whatever you need.</p>
<p>The Leaflet source code is well organized, modularized and easy to understand. I’ve created custom grid layers using different tile sources, with different coordinate systems, animations with frame transitions, custom controls, clickable polygons, popups with dynamic content from AJAX calls, and more. And all works smoothly. So I recommend that you <b>go ahead and start using Leaflet right away</b>.</p>
<p><img src="/blog/2019/03/switching-google-maps-leaflet/leaflet-map-radsat-hd.jpg" /><br><small>Example of a fully-functional Leaflet map with custom controls, overlays, animations and polygons.</small></p>
<p>And this is the repository with my weather map source code: <a href="https://github.com/juanpabloventoso/RadSat-HD">RadSat HD</a>. Feel free to leave any comments or suggestions!</p>
Story telling with Cesiumhttps://www.endpointdev.com/blog/2016/03/story-telling-with-cesium/2016-03-07T00:00:00+00:00Dmitry Kiselev
<h3 id="let-me-tell-you-about-my-own-town">Let me tell you about my own town</h3>
<p>I was born in Yekaterinburg. It’s a middle-sized town in Russia.</p>
<p>Most likely you don’t know where it is. So let me show you:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-html" data-lang="html"><span style="color:#c00;font-weight:bold"><!DOCTYPE html></span>
<<span style="color:#b06;font-weight:bold">html</span> <span style="color:#369">lang</span>=<span style="color:#d20;background-color:#fff0f0">"en"</span>>
<<span style="color:#b06;font-weight:bold">head</span>>
<<span style="color:#b06;font-weight:bold">title</span>>Hello World!</<span style="color:#b06;font-weight:bold">title</span>>
<<span style="color:#b06;font-weight:bold">script</span> <span style="color:#369">src</span>=<span style="color:#d20;background-color:#fff0f0">"/cesium/Build/Cesium/Cesium.js"</span>></<span style="color:#b06;font-weight:bold">script</span>>
<<span style="color:#b06;font-weight:bold">link</span> <span style="color:#369">rel</span>=<span style="color:#d20;background-color:#fff0f0">"stylesheet"</span> <span style="color:#369">href</span>=<span style="color:#d20;background-color:#fff0f0">"layout.css"</span>></<span style="color:#b06;font-weight:bold">link</span>>
</<span style="color:#b06;font-weight:bold">head</span>>
<<span style="color:#b06;font-weight:bold">body</span>>
<<span style="color:#b06;font-weight:bold">div</span> <span style="color:#369">id</span>=<span style="color:#d20;background-color:#fff0f0">"cesiumContainer"</span>></<span style="color:#b06;font-weight:bold">div</span>>
<<span style="color:#b06;font-weight:bold">script</span>>
<span style="color:#080;font-weight:bold">var</span> viewer = <span style="color:#080;font-weight:bold">new</span> Cesium.Viewer(<span style="color:#d20;background-color:#fff0f0">'cesiumContainer'</span>);
(<span style="color:#080;font-weight:bold">function</span>(){
<span style="color:#080;font-weight:bold">var</span> ekb = viewer.entities.add({
name : <span style="color:#d20;background-color:#fff0f0">'Yekaterinburg'</span>,
<span style="color:#888">// Lon, Lat coordinates
</span><span style="color:#888"></span> position : Cesium.Cartesian3.fromDegrees(<span style="color:#00d;font-weight:bold">60.6054</span>, <span style="color:#00d;font-weight:bold">56.8389</span>),
<span style="color:#888">// Styled geometry
</span><span style="color:#888"></span> point : {
pixelSize : <span style="color:#00d;font-weight:bold">5</span>,
color : Cesium.Color.RED
},
<span style="color:#888">// Labeling
</span><span style="color:#888"></span> label : {
text : <span style="color:#d20;background-color:#fff0f0">'Yekaterinburg'</span>,
font : <span style="color:#d20;background-color:#fff0f0">'16pt monospace'</span>,
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
outlineWidth : <span style="color:#00d;font-weight:bold">2</span>,
verticalOrigin : Cesium.VerticalOrigin.BOTTOM,
pixelOffset : <span style="color:#080;font-weight:bold">new</span> Cesium.Cartesian2(<span style="color:#00d;font-weight:bold">0</span>, -<span style="color:#00d;font-weight:bold">9</span>)
}
});
<span style="color:#888">// How to place camera around point
</span><span style="color:#888"></span> <span style="color:#080;font-weight:bold">var</span> heading = Cesium.<span style="color:#038">Math</span>.toRadians(<span style="color:#00d;font-weight:bold">0</span>);
<span style="color:#080;font-weight:bold">var</span> pitch = Cesium.<span style="color:#038">Math</span>.toRadians(-<span style="color:#00d;font-weight:bold">30</span>);
viewer.zoomTo(ekb, <span style="color:#080;font-weight:bold">new</span> Cesium.HeadingPitchRange(heading, pitch, <span style="color:#00d;font-weight:bold">10000</span>));
})();
</<span style="color:#b06;font-weight:bold">script</span>>
</<span style="color:#b06;font-weight:bold">body</span>>
</<span style="color:#b06;font-weight:bold">html</span>>
</code></pre></div><div class="separator" style="clear: both; text-align: left;"><a href="/blog/2016/03/story-telling-with-cesium/image-0-big.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="/blog/2016/03/story-telling-with-cesium/image-0.png"/></a></div>
<p>Now, I would like to tell you about the history of my town. In the beginning it was a fortified metallurgical plant with a few residential blocks and some public buildings. It was relatively small.</p>
<p>I could say that its size was about the size of a modern city-center, but that description is too vague. I think the best way to tell you something about the city is with a map.</p>
<p>I’ll show you two maps.</p>
<ol>
<li>A map from 1750, early in the town’s history when it was just a factory: <a href="http://www.retromap.ru/m.php#r=1417506">http://www.retromap.ru/m.php#r=1417506</a></li>
<li>A map from 1924 at about the start of Soviet industrialization, just before Yekaterinburg became the industrial center of the Urals: <a href="http://www.retromap.ru/m.php#r=1419241">http://www.retromap.ru/m.php#r=1419241</a></li>
</ol>
<p>Thanks for these beautiful maps to <a href="http://www.retromap.ru/">http://www.retromap.ru</a>.</p>
<p>The map from 1750 is a small image, so I’ve added it via <a href="http://cesiumjs.org/Cesium/Build/Documentation/SingleTileImageryProvider.html">SingleTileImageryProvider</a>. Just specifying the src and coordinates:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-js" data-lang="js"><span style="color:#080;font-weight:bold">function</span> setupImagery() {
<span style="color:#080;font-weight:bold">var</span> layers = viewer.scene.imageryLayers;
<span style="color:#080;font-weight:bold">var</span> s = <span style="color:#00d;font-weight:bold">56.8321929</span>;
<span style="color:#080;font-weight:bold">var</span> n = <span style="color:#00d;font-weight:bold">56.8442609</span>;
<span style="color:#080;font-weight:bold">var</span> w = <span style="color:#00d;font-weight:bold">60.5878970</span>;
<span style="color:#080;font-weight:bold">var</span> e = <span style="color:#00d;font-weight:bold">60.6187892</span>;
<span style="color:#080;font-weight:bold">var</span> l1750 = layers.addImageryProvider(<span style="color:#080;font-weight:bold">new</span> Cesium.SingleTileImageryProvider({
url : <span style="color:#d20;background-color:#fff0f0">'assets/1750.png'</span>,
rectangle : Cesium.Rectangle.fromDegrees(w,s,e,n)
}));
l1750.alpha = <span style="color:#00d;font-weight:bold">0.75</span>;
}
</code></pre></div><p>Now you can see how small my town was initially.</p>
<div class="separator" style="clear: both; text-align: left;"><a href="/blog/2016/03/story-telling-with-cesium/image-1-big.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="/blog/2016/03/story-telling-with-cesium/image-1.png"/></a></div>
<p>The second image is larger, so I’ve split it up into tiles. If you use <a href="http://www.gdal.org/gdal2tiles.html">gdal2tiles.py</a> for generating tiles, it creates all the metadata necessary for <a href="http://wiki.osgeo.org/wiki/Tile_Map_Service_Specification">TMS</a> and you are able connect the imagery set via <a href="https://cesiumjs.org/releases/1.17/Build/Documentation/TileMapServiceImageryProvider.html">TileMapServiceImageryProvider</a>. Otherwise, you can use <a href="http://cesiumjs.org/Cesium/Build/Documentation/UrlTemplateImageryProvider.html">UrlTemplateImageryProvider</a>.</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-js" data-lang="js"><span style="color:#080;font-weight:bold">var</span> l1924 = layers.addImageryProvider(<span style="color:#080;font-weight:bold">new</span> Cesium.TileMapServiceImageryProvider({
url: <span style="color:#d20;background-color:#fff0f0">'assets/1924t'</span>,
minimumLevel: <span style="color:#00d;font-weight:bold">8</span>,
maximumLevel: <span style="color:#00d;font-weight:bold">16</span>,
credit: <span style="color:#d20;background-color:#fff0f0">'retromap.ru'</span>
}));
l1924.alpha = <span style="color:#00d;font-weight:bold">0.75</span>;
</code></pre></div><p>I’ve used <a href="http://www.qgis.org/ru/site/">QGIS</a> for geo referencing. Here is a good <a href="http://qgis.spatialthoughts.com/2012/02/tutorial-georeferencing-topo-sheets.html">tutorial</a>.</p>
<h4 id="user-interface-and-angularjs">User interface and Angular.js</h4>
<p>And below here you see how I’ve added some controls for visibility and opacity of the overlays. Later we will bind them with an Angular-driven GUI:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-js" data-lang="js"><span style="color:#888">// APP is global
</span><span style="color:#888"></span>
<span style="color:#080;font-weight:bold">var</span> layersHash = {
<span style="color:#d20;background-color:#fff0f0">'l1750'</span>: l1750,
<span style="color:#d20;background-color:#fff0f0">'l1924'</span>: l1924
}
APP.setAlpha = <span style="color:#080;font-weight:bold">function</span>(layer, alpha) {
<span style="color:#080;font-weight:bold">if</span>(layersHash[layer] && layersHash[layer].alpha !== <span style="color:#080;font-weight:bold">undefined</span>) {
layersHash[layer].alpha = alpha;
}
};
APP.show = <span style="color:#080;font-weight:bold">function</span>(layer) {
<span style="color:#080;font-weight:bold">if</span>(layersHash[layer] && layers.indexOf(layersHash[layer]) < <span style="color:#00d;font-weight:bold">0</span>) {
layers.add(layersHash[layer]);
}
};
</code></pre></div><p>Why not keep our views in a namespace and access them directly from an Angular controller? Using this approach will give us a lot of flexibility:</p>
<ul>
<li>You can split up the GUI and Cesium modules and use something else instead of Cesium or Angular.</li>
<li>You are able to make a proxy for <code>APP</code> and add business logic to the calls made to it.</li>
<li>It just feels right not to meld all parts of the application into one unmanageble mish-mash of code.</li>
</ul>
<p>For the GUI I’ve added a big slider for the timeline, small sliders for layer opacity, checkboxes for visibility, and call APP methods via Angular’s $watch.</p>
<div class="separator" style="clear: both; text-align: left;"><a href="/blog/2016/03/story-telling-with-cesium/image-2-big.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="/blog/2016/03/story-telling-with-cesium/image-2.png"/></a></div>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-js" data-lang="js">$scope.$watch(<span style="color:#d20;background-color:#fff0f0">'slider.value'</span>, <span style="color:#080;font-weight:bold">function</span>() {
<span style="color:#080;font-weight:bold">var</span> v = $scope.slider.options.stepsArray[$scope.slider.value];
<span style="color:#080;font-weight:bold">if</span> (v >= <span style="color:#00d;font-weight:bold">1723</span> && v <= <span style="color:#00d;font-weight:bold">1830</span>) {
$scope.menu.layers[<span style="color:#00d;font-weight:bold">0</span>].active = <span style="color:#080;font-weight:bold">true</span>;
$scope.menu.layers[<span style="color:#00d;font-weight:bold">1</span>].active = <span style="color:#080;font-weight:bold">false</span>;
}
<span style="color:#080;font-weight:bold">if</span> (v > <span style="color:#00d;font-weight:bold">1830</span> && v < <span style="color:#00d;font-weight:bold">1980</span>) {
$scope.menu.layers[<span style="color:#00d;font-weight:bold">0</span>].active = <span style="color:#080;font-weight:bold">false</span>;
$scope.menu.layers[<span style="color:#00d;font-weight:bold">1</span>].active = <span style="color:#080;font-weight:bold">true</span>;
}
<span style="color:#080;font-weight:bold">if</span>(v >= <span style="color:#00d;font-weight:bold">1980</span>) {
$scope.menu.layers[<span style="color:#00d;font-weight:bold">0</span>].active = <span style="color:#080;font-weight:bold">false</span>;
$scope.menu.layers[<span style="color:#00d;font-weight:bold">1</span>].active = <span style="color:#080;font-weight:bold">false</span>;
}
$scope.updateLayers();
});
$scope.updateLayers = <span style="color:#080;font-weight:bold">function</span>() {
<span style="color:#080;font-weight:bold">for</span> (<span style="color:#080;font-weight:bold">var</span> i = <span style="color:#00d;font-weight:bold">0</span>; i < $scope.menu.layers.length; i++) {
<span style="color:#080;font-weight:bold">if</span> ($scope.menu.layers[i].active ) {
APP.show && APP.show($scope.menu.layers[i].name);
}
<span style="color:#080;font-weight:bold">else</span> {
APP.hide && APP.hide($scope.menu.layers[i].name);
}
}
};
</code></pre></div><h4 id="back-to-the-history">Back to the history</h4>
<p>Yekaterinburg was founded on November 7, 1723. This is the date of the first test-run of the forging hammers in the new factory. The original factory design by Tatishew had 40 forging hammers and 4 blast furnaces. That may well have made it the best equipped and most productive factory of its time.</p>
<p>Now I want to add text to the application. Also, I have some cool pictures of the hammers and furnaces. Adding an overlay for text and binding its content is a matter of <em>CSS</em> and <em>ng-include/ng-bind</em> knowledge and it’s a bit out of scope for this post, but let’s push on and add some pictures and link them to the timeline. Cesium has <a href="http://cesiumjs.org/Cesium/Build/Documentation/KmlDataSource.html">KmlDataSource</a> for KML loading and parsing. This is how my application loads and accesses these attributes:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-js" data-lang="js"><span style="color:#080;font-weight:bold">var</span> entityByName = {};
<span style="color:#080;font-weight:bold">var</span> promise = Cesium.KmlDataSource.load(<span style="color:#d20;background-color:#fff0f0">'assets/foto.kml'</span>);
promise.then(<span style="color:#080;font-weight:bold">function</span>(dataSource) {
viewer.dataSources.add(dataSource);
<span style="color:#888">//KML entities
</span><span style="color:#888"></span> <span style="color:#080;font-weight:bold">var</span> entities = dataSource.entities.values;
<span style="color:#080;font-weight:bold">for</span> (<span style="color:#080;font-weight:bold">var</span> i = <span style="color:#00d;font-weight:bold">0</span>; i < entities.length; i++) {
<span style="color:#080;font-weight:bold">var</span> entity = entities[i];
<span style="color:#888">// <Data> attributes, parsed into js object
</span><span style="color:#888"></span> <span style="color:#080;font-weight:bold">var</span> ed = entity.kml.extendedData;
entityByName[entity.id] = {
<span style="color:#d20;background-color:#fff0f0">'entity'</span>: entity,
since: <span style="color:#038">parseInt</span>(ed.since.value),
to: <span style="color:#038">parseInt</span>(ed.to.value)
};
}
});
</code></pre></div><p>Add bindings for Angular:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-js" data-lang="js">APP.filterEtityByY = <span style="color:#080;font-weight:bold">function</span>(y) {
<span style="color:#080;font-weight:bold">for</span> (<span style="color:#080;font-weight:bold">var</span> k <span style="color:#080;font-weight:bold">in</span> entityByName) {
<span style="color:#080;font-weight:bold">if</span>(entityByName.hasOwnProperty(k)) {
<span style="color:#080;font-weight:bold">var</span> s = entityByName[k].since;
<span style="color:#080;font-weight:bold">var</span> t = entityByName[k].to;
entityByName[k].entity.show = (y >= s && y <= t);
}
}
};
<span style="color:#080;font-weight:bold">var</span> heading = Cesium.<span style="color:#038">Math</span>.toRadians(<span style="color:#00d;font-weight:bold">0</span>);
<span style="color:#080;font-weight:bold">var</span> pitch = Cesium.<span style="color:#038">Math</span>.toRadians(-<span style="color:#00d;font-weight:bold">30</span>);
<span style="color:#080;font-weight:bold">var</span> distanceMeters = <span style="color:#00d;font-weight:bold">500</span>;
<span style="color:#080;font-weight:bold">var</span> enityHeading = <span style="color:#080;font-weight:bold">new</span> Cesium.HeadingPitchRange(heading, pitch, distanceMeters);
APP.zoomToEntity = <span style="color:#080;font-weight:bold">function</span>(name) {
<span style="color:#080;font-weight:bold">if</span>(name && entityByName[name]) {
viewer.zoomTo(entityByName[name].entity, enityHeading);
}
};
</code></pre></div><p>I’ve added object timespans via extended data. If you want to use the Cesium/GE default timeline, you should do it via a <em>TimeSpan</em> section in the KML’s entries:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-xml" data-lang="xml"><span style="color:#b06;font-weight:bold"><timespan></span>
<span style="color:#b06;font-weight:bold"><begin></span>2000-01-00T00:00:00Z<span style="color:#b06;font-weight:bold"></begin></span>
<span style="color:#b06;font-weight:bold"><end></span>2000-02-00T00:00:00Z<span style="color:#b06;font-weight:bold"></end></span>
<span style="color:#b06;font-weight:bold"></timespan></span>
</code></pre></div><p>Another interesting fact about my town is that between 1924 and 1991 it had a different name: <em>Sverdlovsk (Свердловск)</em>. So I’ve added town name changing via APP and connected it to the timeline.</p>
<p>Using <em>APP.filterEtityByY</em> and <em>APP.zoomToEntity</em> it’s relatively easy to connect a page hash like <em>example.com/#!/feature/smth</em> with features from KML. One point to note is that I use my own hash’s path part parser instead of ngRoute’s approach.</p>
<div class="separator" style="clear: both; text-align: left;"><a href="/blog/2016/03/story-telling-with-cesium/image-3-big.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="/blog/2016/03/story-telling-with-cesium/image-3.png"/></a></div>
<p>You can see how all these elements work together at <a href="https://dmitry.endpointdev.com/cesium/ekb/">https://dmitry.endpointdev.com/cesium/ekb/</a> and the sources are on GitHub: <a href="https://github.com/kiselev-dv/EkbHistory/tree/master">https://github.com/kiselev-dv/EkbHistory/tree/master</a></p>
Using Google Maps and jQuery for Location Searchhttps://www.endpointdev.com/blog/2014/01/using-google-maps-and-jquery-for/2014-01-16T00:00:00+00:00Steph Skardal
<div class="separator" style="clear: both; text-align: center;margin-bottom:10px;"><img border="0" src="/blog/2014/01/using-google-maps-and-jquery-for/image-0.png"/><br/>
Example of Google maps showing <a href="http://www.paper-source.com/">Paper Source</a> locations.</div>
<p>A few months ago, I built out functionality to display physical store locations within a search radius for <a href="http://www.paper-source.com/">Paper Source</a> on an interactive map. There are a few map tools out there to help accomplish this goal, but I chose Google Maps because of my familiarity and past success using it. Here I’ll go through some of the steps to implement this functionality.</p>
<h3 id="google-maps-api-key">Google Maps API Key</h3>
<p>Before you start this work, you’ll want to get a Google Maps API key. Learn more <a href="https://developers.google.com/maps/documentation/javascript/tutorial#api_key">here</a>.</p>
<h3 id="geocoder-object">Geocoder Object</h3>
<p>At the core of our functionality is the use of the <a href="https://developers.google.com/maps/documentation/javascript/geocoding">google.maps.Geocoder</a> object. The Geocoder converts a search point or search string to into geographic coordinates. The most basic use of the geocoder might look like this:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-javascript" data-lang="javascript"><span style="color:#080;font-weight:bold">var</span> geocoder = <span style="color:#080;font-weight:bold">new</span> google.maps.Geocoder();
<span style="color:#888">//search is a string, input by user
</span><span style="color:#888"></span>geocoder.geocode({ <span style="color:#d20;background-color:#fff0f0">'address'</span> : search }, <span style="color:#080;font-weight:bold">function</span>(results, status) {
<span style="color:#080;font-weight:bold">if</span>(status == <span style="color:#d20;background-color:#fff0f0">"ZERO_RESULTS"</span>) {
<span style="color:#888">//Indicate to user no location has been found
</span><span style="color:#888"></span> } <span style="color:#080;font-weight:bold">else</span> {
<span style="color:#888">//Do something with resulting location(s)
</span><span style="color:#888"></span> }
}
</code></pre></div><h3 id="rendering-a-map-from-the-results">Rendering a Map from the Results</h3>
<p>After a geocoder results set is acquired, a map and locations might be displayed. A simple and standard implementation of Google Maps can be executed, with the map center set to the geocoder results set center:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-javascript" data-lang="javascript"><span style="color:#080;font-weight:bold">var</span> mapOptions = {
center: results[<span style="color:#00d;font-weight:bold">0</span>].geometry.bounds.getCenter(),
zoom: <span style="color:#00d;font-weight:bold">10</span>,
mapTypeId: google.maps.MapTypeId.ROADMAP
};
<span style="color:#080;font-weight:bold">var</span> map = <span style="color:#080;font-weight:bold">new</span> google.maps.Map(<span style="color:#038">document</span>.getElementById(<span style="color:#d20;background-color:#fff0f0">"map"</span>), mapOptions);
</code></pre></div><h3 id="searching-within-a-radius">Searching within a Radius</h3>
<p>Next up, you may want to figure out how to display a set of locations inside the map bounds. At the time I implemented the code, I found no functionality that automagically did this, so I based my solution off of a few references I found online. The following code excerpt steps through the process:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-javascript" data-lang="javascript"><span style="color:#888">//search center is the center of the geocoded location
</span><span style="color:#888"></span><span style="color:#080;font-weight:bold">var</span> search_center = results[<span style="color:#00d;font-weight:bold">0</span>].geometry.bounds.getCenter();
<span style="color:#888">//Earth's radius, used in distance calculation
</span><span style="color:#888"></span><span style="color:#080;font-weight:bold">var</span> R = <span style="color:#00d;font-weight:bold">6371</span>;
<span style="color:#888">//Step through each location
</span><span style="color:#888"></span>$.each(all_locations, <span style="color:#080;font-weight:bold">function</span>(i, loc) {
<span style="color:#888">//Calculate distance from map center
</span><span style="color:#888"></span> <span style="color:#080;font-weight:bold">var</span> loc_position = <span style="color:#080;font-weight:bold">new</span> google.maps.LatLng(loc.latitude, loc.longitude);
<span style="color:#080;font-weight:bold">var</span> dLat = locations.rad(loc.latitude - search_center.lat());
<span style="color:#080;font-weight:bold">var</span> dLong = locations.rad(loc.longitude - search_center.lng());
<span style="color:#080;font-weight:bold">var</span> a = <span style="color:#038">Math</span>.sin(dLat/<span style="color:#00d;font-weight:bold">2</span>) * <span style="color:#038">Math</span>.sin(dLat/<span style="color:#00d;font-weight:bold">2</span>) +
<span style="color:#038">Math</span>.cos(locations.rad(search_center.lat())) *
<span style="color:#038">Math</span>.cos(locations.rad(search_center.lat())) *
<span style="color:#038">Math</span>.sin(dLong/<span style="color:#00d;font-weight:bold">2</span>) * <span style="color:#038">Math</span>.sin(dLong/<span style="color:#00d;font-weight:bold">2</span>);
<span style="color:#080;font-weight:bold">var</span> c = <span style="color:#00d;font-weight:bold">2</span> * <span style="color:#038">Math</span>.atan2(<span style="color:#038">Math</span>.sqrt(a), <span style="color:#038">Math</span>.sqrt(<span style="color:#00d;font-weight:bold">1</span>-a));
<span style="color:#080;font-weight:bold">var</span> d = R * c;
loc.distance = d;
<span style="color:#888">//Add the marker to the map
</span><span style="color:#888"></span> <span style="color:#080;font-weight:bold">var</span> marker = <span style="color:#080;font-weight:bold">new</span> google.maps.Marker({
map: map,
position: loc_position,
title: loc.store_title
});
<span style="color:#888">//Convert distance to miles (readable distance) for display purposes
</span><span style="color:#888"></span> loc.readable_distance = (google.maps.geometry.spherical.
computeDistanceBetween(search_center, loc_position)*
<span style="color:#00d;font-weight:bold">0.000621371</span>).toFixed(<span style="color:#00d;font-weight:bold">2</span>);
});
</code></pre></div><p>The important thing about this code is that it renders markers for all the locations, but a subset of them will be visible.</p>
<h3 id="figuring-out-which-locations-are-visible">Figuring out which Locations are Visible</h3>
<p>If you want to display additional information in the HTML related to current visible locations (such as in the screenshot at the top of this post), you might consider using the map.getBounds.contains() method:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-javascript" data-lang="javascript"><span style="color:#080;font-weight:bold">var</span> render_locations = <span style="color:#080;font-weight:bold">function</span>(map) {
<span style="color:#080;font-weight:bold">var</span> included_locations = [];
<span style="color:#888">//Loop through all locations to determine which locations are contained in the map boundary
</span><span style="color:#888"></span> $.each(all_locations, <span style="color:#080;font-weight:bold">function</span>(i, loc) {
<span style="color:#080;font-weight:bold">if</span>(map.getBounds().contains(<span style="color:#080;font-weight:bold">new</span> google.maps.LatLng(loc.latitude, loc.longitude))) {
<span style="color:#00d;font-weight:bold">_</span>push(loc);
}
}
<span style="color:#888">// sort locations by distance if desired
</span><span style="color:#888"></span>
<span style="color:#888">// render included_locations
</span><span style="color:#888"></span>};
</code></pre></div><p>The above code determines which locations are visible, sorts those locations by readable distance, and then those locations are rendered in the HTML.</p>
<h3 id="adding-listeners">Adding Listeners</h3>
<p>After you’ve got your map and location markers added, a few map listeners will add more functionality, described below:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-javascript" data-lang="javascript"><span style="color:#080;font-weight:bold">var</span> listener = google.maps.event.addListener(map, <span style="color:#d20;background-color:#fff0f0">"idle"</span>, <span style="color:#080;font-weight:bold">function</span>() {
render_locations(map);
google.maps.event.addListener(map, <span style="color:#d20;background-color:#fff0f0">'center_changed'</span>, <span style="color:#080;font-weight:bold">function</span>() {
render_locations(map);
});
google.maps.event.addListener(map, <span style="color:#d20;background-color:#fff0f0">'zoom_changed'</span>, <span style="color:#080;font-weight:bold">function</span>() {
render_locations(map);
});
});
</code></pre></div><p>After the map has loaded (via the map “idle” event), render_locations is called to render the HTML for visible locations. This method is also triggered any time the map center or zoom level is changed, so the HTML to the left of the map is updated whenever a user modifies the map bounds.</p>
<h3 id="advanced-elements">Advanced Elements</h3>
<p>Two advanced pieces implemented were the use of extending the map bounds and modifying listeners in a mobile environment. When it was desired that the map explicitly contain a set of locations within the map bounds, the following code was used:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-javascript" data-lang="javascript"><span style="color:#080;font-weight:bold">var</span> current_bounds = results[<span style="color:#00d;font-weight:bold">0</span>].geometry.bounds;
$.each(locations_to_include, <span style="color:#080;font-weight:bold">function</span>(i, loc) {
current_bounds.extend(loc_position);
});
map.fitBounds(current_bounds);
</code></pre></div><p>And in a mobile environment, it was desired to disable various map options such as draggability, zoomability, and scroll wheel use. This was done with the following conditional:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-javascript" data-lang="javascript"><span style="color:#080;font-weight:bold">if</span>($(<span style="color:#038">window</span>).width() < <span style="color:#00d;font-weight:bold">656</span>) {
map.setOptions({
draggable: <span style="color:#080;font-weight:bold">false</span>,
zoomControl: <span style="color:#080;font-weight:bold">false</span>,
scrollwheel: <span style="color:#080;font-weight:bold">false</span>,
scrollwheel: <span style="color:#080;font-weight:bold">false</span>,
disableDoubleClickZoom: <span style="color:#080;font-weight:bold">true</span>,
streetViewControl: <span style="color:#080;font-weight:bold">false</span>
});
}
</code></pre></div><h3 id="conclusion">Conclusion</h3>
<p>Of course, all the code shown above is just in snippet form. Many of the building blocks described above were combined to build a user-friendly map feature. There are a lot of additional map features – <a href="https://developers.google.com/maps/documentation/webservices/">check out the documentation</a> to learn more.</p>
End Point Liquid Galaxy Projects at Google I/O 2013https://www.endpointdev.com/blog/2013/05/end-point-liquid-galaxy-projects-at/2013-05-23T00:00:00+00:00Dave Jenkins
<p>This last week End Point participated in the Google I/O conference for the third year in a row. As the lead agency for Liquid Galaxy development and deployment, our engineers were active in the development efforts for the two Liquid Galaxy systems that were showcased at the conference this year.</p>
<p>We sent two of our rock stars to the show, Kiel and Matt. This year both Liquid Galaxies used Google Maps API functionality in the browser rather than the stand-alone Google Earth app:</p>
<ul>
<li>
<p>Treadmill-driven Google Trekker: Working with <a href="https://wearesparks.com/">Sparks Online</a>, this showed a treadmill connected to the Google Trekker Trails panoramic imagery. Walking on the treadmill moves the view forward through the Bright Angel Trail in Grand Canyon. The tricky part being the curves in the trail, especially switchbacks: with no mouse to adjust the view, how to keep the view on the path when the input (the movement of the treadmill) was just “straight forward”? Our engineer, Kiel, used functions around Maps API data to automatically calculate the “closest frame” that would be next in line, and then force-feeds it to the Trail View, so “forward” is always centered on the path, no matter if the next frame is actually five to ten degrees (or in the case of the switchbacks, up to 150 degrees) left or right of center.</p>
</li>
<li>
<p>WebGL Skydiving Game: In support of the creative agency <a href="http://www.instrument.com/">Instrument</a>, Matt provided expert consulting leading up to the show on the Liquid-Galaxy-enabling of the WebGL game demo Instrument developed that allows people to “skydive” down thru a series of rings suspended in mid-air. Maybe it’s just better to show the game:</p>
</li>
</ul>
<iframe width="560" height="315" src="https://www.youtube.com/embed/FghhA-hLBg8" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>
<p>End Point’s full support included equipment rental to Instrument for development, developing and configuring the new Liquid Galaxy used in the Skydiving game, setting up the Liquid Galaxies at the show, onsite support, and repacking of the systems at the conclusion of the conference. We make things as turnkey as possible.</p>
<p>Kiel said the following: “Google I/O is getting bigger, more interesting, and more packed every year. The GLASS track was a lot of fun”. Matt added “Google is as committed as ever to user experience, and they happily share all of their tricks with developers year after year.”</p>
<p>Matt and Kiel appreciated the hands-on support of Andreu Ibàñez, from End Point’s partner, <a href="http://www.ponent2002.com/">Ponent 2002</a>, in the physical setup of the Liquid Galaxies we installed at the show and his sharing in the staffing of the skydiving exhibit.</p>
<p>Feedback from show attendees was overwhelmingly positive, the whole Google Maps area drawing quite a crowd to take part in each experience.</p>
<p>End Point welcomes opportunities to work with creative agencies and event planners to build unique and compelling visualization experiences that utilize the Liquid Galaxy platform. Please contact us if you’ve got an idea. Also, see our <a href="https://www.visionport.com">Liquid Galaxy website</a> to see some of the many uses for the system.</p>
Paper Source Case Study with Google Maps APIhttps://www.endpointdev.com/blog/2013/03/paper-source-case-study-with-google/2013-03-29T00:00:00+00:00Steph Skardal
<img alt="Basic Google map with location markers" border="0" src="/blog/2013/03/paper-source-case-study-with-google/image-0.png"/>
<p>Recently, I’ve been working with the <a href="https://developers.google.com/maps/">Google Maps API</a> for Paper Source, one of our large <a href="/expertise/perl-interchange/">Interchange</a> clients with over 40 physical stores throughout the US.</p>
<p>On their website, they had previously been managing static HTML pages for these 40 physical stores to share store information, location, and hours. They wanted to move in the direction of something more dynamic with interactive maps. After doing a bit of research on search options out there, I decided to go with the Google Maps API. This article discusses basic implementation of map rendering, search functionality, as well as interesting edge case behavior.</p>
<h3 id="basic-map-implementation">Basic Map Implementation</h3>
<p>In its most simple form, the markup required for adding a basic map with markers is the shown below. Read more at <a href="https://developers.google.com/maps/">Google Maps Documentation</a>.</p>
<h5 id="html">HTML</h5>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-html" data-lang="html"><<span style="color:#b06;font-weight:bold">div</span> <span style="color:#369">id</span>=<span style="color:#d20;background-color:#fff0f0">"map"</span>></<span style="color:#b06;font-weight:bold">div</span>>
</code></pre></div><h5 id="css">CSS</h5>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-html" data-lang="html">#map {
height: 500px;
width: 500px;
}
</code></pre></div><h5 id="javascript">JavaScript</h5>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-javascript" data-lang="javascript"><span style="color:#888">//mapOptions defined here
</span><span style="color:#888"></span><span style="color:#080;font-weight:bold">var</span> mapOptions = {
center: <span style="color:#080;font-weight:bold">new</span> google.maps.LatLng(<span style="color:#00d;font-weight:bold">40</span>, -<span style="color:#00d;font-weight:bold">98</span>),
zoom: <span style="color:#00d;font-weight:bold">3</span>,
mapTypeId: google.maps.MapTypeId.ROADMAP
};
<span style="color:#888">//map is the HTML DOM element ID where it will be rendered
</span><span style="color:#888"></span><span style="color:#080;font-weight:bold">var</span> map = <span style="color:#080;font-weight:bold">new</span> google.maps.Map(<span style="color:#038">document</span>.getElementById(<span style="color:#d20;background-color:#fff0f0">"map"</span>), mapOptions);
<span style="color:#888">//all locations is a JSON object representing locations,
</span><span style="color:#888">//where each location has a latitude and longitude
</span><span style="color:#888"></span>$.each(all_locations, <span style="color:#080;font-weight:bold">function</span>(i, loc) {
<span style="color:#080;font-weight:bold">var</span> marker = <span style="color:#080;font-weight:bold">new</span> google.maps.Marker({
map: map,
position: <span style="color:#080;font-weight:bold">new</span> google.maps.LatLng(loc.latitude, loc.longitude)
});
})
</code></pre></div><h3 id="building-search-functionality">Building Search Functionality</h3>
<img alt="Search interface: search results are listed on the left, and map with markers is shown on the right" border="0" src="/blog/2013/03/paper-source-case-study-with-google/image-1.png" width="700"/>
<p>Next up, I needed to build out search functionality. Google has its own geocoder to allow address searches. Here is the basic markup for running a search:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-javascript" data-lang="javascript"><span style="color:#080;font-weight:bold">var</span> geocoder = <span style="color:#080;font-weight:bold">new</span> google.maps.Geocoder();
<span style="color:#888">//search is a variable representing the user search, such as a zip code, city name, or state name
</span><span style="color:#888"></span>geocoder.geocode({ <span style="color:#d20;background-color:#fff0f0">'address'</span> : search }, <span style="color:#080;font-weight:bold">function</span>(results, status) {
<span style="color:#080;font-weight:bold">var</span> search_center = results[<span style="color:#00d;font-weight:bold">0</span>].geometry.bounds.getCenter();
<span style="color:#080;font-weight:bold">var</span> mapOptions = {
center: search_center,
zoom: <span style="color:#00d;font-weight:bold">10</span>,
mapTypeId: google.maps.MapTypeId.ROADMAP
};
<span style="color:#080;font-weight:bold">var</span> map = <span style="color:#080;font-weight:bold">new</span> google.maps.Map(<span style="color:#038">document</span>.getElementById(<span style="color:#d20;background-color:#fff0f0">"map"</span>), mapOptions);
$.each(all_locations, <span style="color:#080;font-weight:bold">function</span>(i, loc) {
<span style="color:#080;font-weight:bold">var</span> marker = <span style="color:#080;font-weight:bold">new</span> google.maps.Marker({
map: map,
position: <span style="color:#080;font-weight:bold">new</span> google.maps.LatLng(loc.latitude, loc.longitude)
});
})
}
</code></pre></div><p>In the above code, the search term is passed into the Geocoder object and a map with all locations marked is rendered. To determine which markers are in the visible map boundaries, the following map.getBounds().contains() method would be leveraged:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-javascript" data-lang="javascript"><span style="color:#080;font-weight:bold">var</span> visible_locations = [];
$.each(all_locations, <span style="color:#080;font-weight:bold">function</span>(i, loc) {
<span style="color:#080;font-weight:bold">if</span>(map.getBounds().contains(<span style="color:#080;font-weight:bold">new</span> google.maps.LatLng(loc.latitude, loc.longitude))) {
visible_locations.push(loc);
}
});
<span style="color:#888">//render visible locations to the left of the map
</span></code></pre></div><p>One final step here is to add a listener to the map, so that visible locations are updated when the user zooms in and out. This is accomplished with the following listener:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-javascript" data-lang="javascript">google.maps.event.addListener(map, <span style="color:#d20;background-color:#fff0f0">'zoom_changed'</span>, <span style="color:#080;font-weight:bold">function</span>() {
<span style="color:#888">//call method to rerender visible locations
</span><span style="color:#888"></span>});
</code></pre></div><h3 id="handling-zero-results">Handling Zero Results</h3>
<p>What happens if your Geocoder object can’t find the address? A simple conditional can be used:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-javascript" data-lang="javascript">geocoder.geocode({ <span style="color:#d20;background-color:#fff0f0">'address'</span> : search }, <span style="color:#080;font-weight:bold">function</span>(results, status) {
<span style="color:#080;font-weight:bold">if</span>(status == <span style="color:#d20;background-color:#fff0f0">"ZERO_RESULTS"</span>) {
<span style="color:#888">//notify customer that no results have been found
</span><span style="color:#888"></span> } <span style="color:#080;font-weight:bold">else</span> {
<span style="color:#888">//got results, render location
</span><span style="color:#888"></span> }
}
</code></pre></div><h3 id="calculate-and-sort-by-distance">Calculate and Sort by Distance</h3>
<p>The next layer of logic I needed to add was the ability to determine the distance between the search address and sort the results by distance. To calculate distance, I did some research and settled on the following code:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-javascript" data-lang="javascript"><span style="color:#080;font-weight:bold">var</span> R = <span style="color:#00d;font-weight:bold">6371</span>;
$.each(all_locations, <span style="color:#080;font-weight:bold">function</span>(i, loc) {
<span style="color:#080;font-weight:bold">var</span> loc_position = <span style="color:#080;font-weight:bold">new</span> google.maps.LatLng(loc.latitude, loc.longitude);
<span style="color:#080;font-weight:bold">var</span> dLat = locations.rad(loc.latitude - search_center.lat());
<span style="color:#080;font-weight:bold">var</span> dLong = locations.rad(loc.longitude - search_center.lng());
<span style="color:#888">//calculate spherical distance between search position and location
</span><span style="color:#888"></span> <span style="color:#080;font-weight:bold">var</span> a = <span style="color:#038">Math</span>.sin(dLat/<span style="color:#00d;font-weight:bold">2</span>) * <span style="color:#038">Math</span>.sin(dLat/<span style="color:#00d;font-weight:bold">2</span>) +
<span style="color:#038">Math</span>.cos(locations.rad(search_center.lat())) *
<span style="color:#038">Math</span>.cos(locations.rad(search_center.lat())) *
<span style="color:#038">Math</span>.sin(dLong/<span style="color:#00d;font-weight:bold">2</span>) * <span style="color:#038">Math</span>.sin(dLong/<span style="color:#00d;font-weight:bold">2</span>);
<span style="color:#080;font-weight:bold">var</span> c = <span style="color:#00d;font-weight:bold">2</span> * <span style="color:#038">Math</span>.atan2(<span style="color:#038">Math</span>.sqrt(a), <span style="color:#038">Math</span>.sqrt(<span style="color:#00d;font-weight:bold">1</span>-a));
<span style="color:#080;font-weight:bold">var</span> d = R * c;
loc.distance = d;
<span style="color:#888">//convert distance to miles
</span><span style="color:#888"></span> loc.readable_distance =
(google.maps.geometry.spherical.computeDistanceBetween(search_center, loc_position) *
<span style="color:#00d;font-weight:bold">0.000621371</span>).toFixed(<span style="color:#00d;font-weight:bold">2</span>);
});
</code></pre></div><p>To sort the locations by distance, I leverage jQuery sort:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-javascript" data-lang="javascript"><span style="color:#080;font-weight:bold">var</span> sort_by_distance = <span style="color:#080;font-weight:bold">function</span>(obj) {
<span style="color:#080;font-weight:bold">return</span> obj.sort(<span style="color:#080;font-weight:bold">function</span>(a, b) {
<span style="color:#080;font-weight:bold">if</span>(a.distance > b.distance) {
<span style="color:#080;font-weight:bold">return</span> <span style="color:#00d;font-weight:bold">1</span>;
} <span style="color:#080;font-weight:bold">else</span> {
<span style="color:#080;font-weight:bold">return</span> -<span style="color:#00d;font-weight:bold">1</span>;
}
})
};
<span style="color:#080;font-weight:bold">var</span> sorted_locations = sort_by_distance(all_locations);
</code></pre></div><h3 id="adjust-map-boundaries-to-include-specific-markers">Adjust Map Boundaries to Include Specific Markers</h3>
<p>Another interesting use case I needed to handle was forcing the map to zoom out to include stores within 100 miles if there was nothing in the initial map boundaries, e.g.:</p>
<img border="0" src="/blog/2013/03/paper-source-case-study-with-google/image-2.png" width="700"/>
<p>The search for “27103” doesn’t return any nearby stores, so the map is extended to include stores within 100 miles.</p>
<p>To accomplish this functionality, I added a bit of code to extend the map boundaries:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-javascript" data-lang="javascript">geocoder.geocode({ <span style="color:#d20;background-color:#fff0f0">'address'</span> : search }, <span style="color:#080;font-weight:bold">function</span>(results, status) {
<span style="color:#080;font-weight:bold">var</span> search_center = results[<span style="color:#00d;font-weight:bold">0</span>].geometry.bounds.getCenter();
<span style="color:#080;font-weight:bold">var</span> mapOptions = {
center: search_center,
zoom: <span style="color:#00d;font-weight:bold">10</span>,
mapTypeId: google.maps.MapTypeId.ROADMAP
};
<span style="color:#080;font-weight:bold">var</span> map = <span style="color:#080;font-weight:bold">new</span> google.maps.Map(<span style="color:#038">document</span>.getElementById(<span style="color:#d20;background-color:#fff0f0">"map"</span>), mapOptions);
<span style="color:#080;font-weight:bold">var</span> current_bounds = results[<span style="color:#00d;font-weight:bold">0</span>].geometry.bounds;
$.each(all_locations, <span style="color:#080;font-weight:bold">function</span>(i, loc) {
<span style="color:#080;font-weight:bold">var</span> loc_position = <span style="color:#080;font-weight:bold">new</span> google.maps.LatLng(loc.latitude, loc.longitude);
<span style="color:#080;font-weight:bold">var</span> dLat = locations.rad(loc.latitude - search_center.lat());
<span style="color:#080;font-weight:bold">var</span> dLong = locations.rad(loc.longitude - search_center.lng());
<span style="color:#888">//calculate spherical distance between search position and location
</span><span style="color:#888"></span> <span style="color:#080;font-weight:bold">var</span> a = <span style="color:#038">Math</span>.sin(dLat/<span style="color:#00d;font-weight:bold">2</span>) * <span style="color:#038">Math</span>.sin(dLat/<span style="color:#00d;font-weight:bold">2</span>) +
<span style="color:#038">Math</span>.cos(locations.rad(search_center.lat())) *
<span style="color:#038">Math</span>.cos(locations.rad(search_center.lat())) *
<span style="color:#038">Math</span>.sin(dLong/<span style="color:#00d;font-weight:bold">2</span>) * <span style="color:#038">Math</span>.sin(dLong/<span style="color:#00d;font-weight:bold">2</span>);
<span style="color:#080;font-weight:bold">var</span> c = <span style="color:#00d;font-weight:bold">2</span> * <span style="color:#038">Math</span>.atan2(<span style="color:#038">Math</span>.sqrt(a), <span style="color:#038">Math</span>.sqrt(<span style="color:#00d;font-weight:bold">1</span>-a));
<span style="color:#080;font-weight:bold">var</span> d = R * c;
loc.distance = d;
<span style="color:#888">//convert distance to miles
</span><span style="color:#888"></span> loc.readable_distance =
(google.maps.geometry.spherical.computeDistanceBetween(search_center, loc_position) *
<span style="color:#00d;font-weight:bold">0.000621371</span>).toFixed(<span style="color:#00d;font-weight:bold">2</span>);
<span style="color:#080;font-weight:bold">var</span> marker = <span style="color:#080;font-weight:bold">new</span> google.maps.Marker({
map: map,
position: <span style="color:#080;font-weight:bold">new</span> google.maps.LatLng(loc.latitude, loc.longitude)
});
<span style="color:#080;font-weight:bold">if</span>(loc.readable_distance < <span style="color:#00d;font-weight:bold">100</span>) {
current_bounds.extend(loc_position);
}
});
<span style="color:#888">//Google map method to fit map boundaries to desired boundaries
</span><span style="color:#888"></span> map.fitBounds(current_bounds);
}
</code></pre></div><h3 id="disable-scroll-and-zoom-on-mobile-sized-devices">Disable Scroll and Zoom on Mobile-Sized Devices</h3>
<p>One final behavior needed was to disable map zooming and scrolling on mobile devices, to improve the usability on mobile/touch interfaces. Here’s how this was accomplished:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-javascript" data-lang="javascript"><span style="color:#080;font-weight:bold">var</span> options_listener = google.maps.event.addListener(map, <span style="color:#d20;background-color:#fff0f0">"idle"</span>, <span style="color:#080;font-weight:bold">function</span>() {
<span style="color:#080;font-weight:bold">if</span>($(<span style="color:#038">window</span>).width() < <span style="color:#00d;font-weight:bold">656</span>) {
map.setOptions({
draggable: <span style="color:#080;font-weight:bold">false</span>,
zoomControl: <span style="color:#080;font-weight:bold">false</span>,
scrollwheel: <span style="color:#080;font-weight:bold">false</span>,
disableDoubleClickZoom: <span style="color:#080;font-weight:bold">true</span>,
streetViewControl: <span style="color:#080;font-weight:bold">false</span>
});
}
google.maps.event.removeListener(options_listener);
});
</code></pre></div><h3 id="conclusion">Conclusion</h3>
<p>With all this code, the final location search functionality includes:</p>
<ul>
<li>Basic United States map rendering to display all physical store locations.</li>
<li>Search by location which shows stores within 100 miles, and allows users to zoom in and out to adjust their search. Search lists results sorted by distance.</li>
<li>“Saved” or “Quick” searches by states, which displays all physical stores by state.</li>
<li>Adjustment of mobile display map options.</li>
</ul>