•Try•
Catch

Ski Map

Create a map of Ski Resorts with MODX

In this tutorial we are going to examine using MODX Revolution to create an interactive HTML5 map that displays dozens of Ski Resorts across North America. Clicking on a pin on the map will display Resort specific content in the right column. Regions will display a list of pins. We won’t be writing any PHP or MySQL just making clever use of what the MODX Manager has to offer. This tutorial will focus on using the Resource Tree to nest multiple depths of Regions, working with MODX Property Sets to keep from repeating ourselves, and using configurable System Settings to control things like whether or not map content should be loaded using AJAX.

Table of Contents


 

Getting Started

We aren’t going to build this entire site from scratch, that would take too long. This source of this tutorial (everything you need) is publicly available for you to follow along. To get started:

  • Sign Up for a Free MODX Cloud Lab Account (if needed)
  • Import this vapor Snapshot to your MODX Cloud Vault (see Import Your Snapshot for more info)
  • Find the imported snapshot in your vault and click Create new Cloud from Snapshot
  • (Wait to receive two notifications one saying the new cloud has been setup another saying the snapshot has been injected.)
  • Create yourself an Admin user. You’ll use these credentials to log into the Manager
  • Click View Manager, log in and get started! (If your new cloud throws errors click the Reinstall MODX button and choose 2.2.7, wait for a notification and all will be well)

Note: You may be wondering why this Example Site isn’t released as a MODX Extra so that it can be installed outside of MODX Cloud. I’ve started packaging it that way but it isn’t ready for prime time yet. If you are interested in obtaining a more traditional package please leave a comment or contact me.


Resort Template

Since we are going to be creating Ski Resorts, the first thing to do would be to create a new Template called Ski Resort. If you’re following along using the provided source this template is already created for you, open it up and have a look. You’ll notice this template is only doing one thing, including a SkiMap chunk.

[[$SkiMap]]

Seriously that’s all it does.


Map Chunks

This Chunk will generate the markup for the entire page of Ski Resorts and eventually Regions as well. Sure it could go directly in the Ski Resort template, but since there’s a chance it may be reused or abstracted later, we are going to place everything in a Chunk. Similar to using functions in JavaScript or classes in CSS, Chunks allow us to keep from repeating ourselves. So find the SkiMap Chunk in the elements tab of your Resource Tree and have a look.

[[$bx-head-open]]
<!-- ^^ doctype, meta, css & modernizr -->
<!-- anything to add before closing the head tag  -->
[[$bx-head-append]]
[[$bx-head-close]]
<!-- open the container -->
[[$bx-container-open]] 
<div id="wrapper">
      <div class="tr" style="">
      <div class="td map">
        <!-- using a context setting for this id -->
         <div id="[[++map_canvas]]">
           <ul>
            <!-- list all the resorts -->
            [[$ListResorts@Map? &parent=`[[++resorts_parent]]`]]
          </ul>
        </div>
      </div>
      <aside class="td map-side" style="">
        <div class="fixed">
          <div class="content">
              <h1>
          <!-- set the menutitle or pagetitle -->
          <span>[[*menutitle:empty=`[[*pagetitle]]`]]</span>[[+icon]]              
              </h1>
              
              
        <!-- if i have a parent, view others in it -->
        [[[[*parent:notempty=`+view-others-in`]]]]
        <!-- ok, the actual content -->
        [[*content]]
        <!-- print view others again -->
        [[[[*parent:notempty=`$view-others-in`]]]]
          </div>
        </div>
      </aside>
    </div>
</div>
[[$bx-container-close]]
<!-- javascript includes -->
[[$bx-bottom-open]]
<!-- some inline javascript -->
[[$map-script]]
<!-- analytics (if set), close body and html -->
[[$bx-bottom-close]]

Ok a lot going on so let’s break it down. Want to view the HTML source of what’s generated above? We’re using boilerX, a MODX Extra to add HTML5 Boilerpate to our page. boilerX comes with several convenient System Settings that allow us to do things like add our Google Analytics tracking ID or set which version of IE the Chrome Frame messaging should be displayed without having to touch any code. By using MODX Chunks, it also encourages us to keep the boilerplate parts of our markup self contained from our project specific code, making it easier to update down the line.

So, back to the above Chunk…

Open the Header

[[$bx-head-open]]

[[$bx-head-open]]
<!-- ^^ doctype, meta, css & modernizr -->
<!-- anything to add before closing the head tag  -->
  • declare HTML5 doctype
  • set title and meta tags
  • include CSS file
  • include modernizr.js

[[$bx-head-append]]

[[$bx-head-append]]
  • include social tags
  • add google fonts
  • set a base tag

[[$bx-head-close]]

[[$bx-head-close]]
  • close the head tag

Our Map Canvas, Pins, and Sidebar

<div id="wrapper">
      <div class="tr" style="">
      <div class="td map">
        <!-- using a context setting for this id -->
         <div id="[[++map_canvas]]">
           <ul>
            <!-- list all the resorts -->
            [[$ListResorts@Map? &parent=`[[++resorts_parent]]`]]
          </ul>
        </div>
      </div>
      <aside class="td map-side" style="">
        <div class="fixed">
          <div class="content">
              <h1>
          <!-- set the menutitle or pagetitle -->
          <span>[[*menutitle:empty=`[[*pagetitle]]`]]</span>[[+icon]]              
              </h1>
        <!-- if i have a parent, view others in it -->
        [[[[*parent:notempty=`+view-others-in`]]]]
        <!-- ok, the actual content -->
        [[*content]]
        <!-- print view others again -->
        [[[[*parent:notempty=`$view-others-in`]]]]
          </div>
        </div>
      </aside>
    </div>
</div>
  • Create a canvas div for Google maps
  • List all the resources using the ListResorts Chunks
  • Pass in the id of the “ultimate parent” for map pins
  • Create a sidebar
  • Set resort title
  • Display show others links
  • Display resort content

Close our Template

[[$bx-container-close]]

[[$bx-container-close]] In our case, this chunk is completely empty. If additional divs needed to be closed, or any other markup before including JavaScript files could go in this chunk.

[[$bx-bottom-open]]

[[$bx-bottom-open]]
  • Include jQuery
  • Include jQuery address
  • Include plugin and main JavaScript files

[[$map-script]]

[[$map-script]]
  • Custom map JavaScript
  • AJAX listeners

[[$bx-bottom-close]]

<!-- analytics (if set), close body and html -->
[[$bx-bottom-close]]
  • Google Analytics (if set)
  • Close body tag
  • Close html tag

List Resorts

The List Resorts Chunk accepts a parent resource and lists all Ski Resorts it contains. It uses the getResources Extra to generate a huge list of all our map pins. A default Property Set is used to define several default values such as depth, sortby and the template to use for rendering each list item.

[[getResources?
&parents=`[[+parent]]`
&where=`{"template:=":[[++resort_template]]}`
&sortby=`[[+sortby]]`
&tpl=`[[+tpl]]`
&limit=`0`
&includeTVs=`1`
&includeTVList=`gps.latitude,gps.longitude,ridden`
&depth=`[[+depth]]`
]]

You can view the getResources Documentation for more info but I’ll explain the properties being used above. You may have wondered how we are going to accomplish creating such a dynamic site with multiple levels of content without writing any PHP or MySQL. This is it!

Note that we are calling the above getResources tag cached ([[getResources]]) meaning we are allowing its results to survive in the MODX Resource cache. If we were to call the tag uncached ([[!getResources]]) it would ensure that the tag is executed fresh on each request, but that is unnecessary and would make our site slow. MODX intuitively clears the resource cache for you, allowing you to keep things cached without having to worry about outdated content on your website.

parents
We pass in our ultimate parent of [[++resorts_parent]] because it’s the top most Resource we want to search. In other words, it contains all the map pins.

where

We use the where property to say only grab resources with a Template ID of our [[++region_template]] Context Setting.

sortby

In our default property set of ListResorts we have [[+sortby]] set to sort by pagetitle in ASC order

tpl

Also in our default Property Set of ListResorts we have [[+tpl]] set to map-pin-item meaning the [[$map-pin-item]] Chunk will be used to render each of our results.

limit

We set limit to 0, meaning return an unlimited number of results.

includeTVs

Since we are going to use tv values like whether or not the resort has been ridden, and its GPS coordinates we set includeTVs to true.

includeTVList

As an optimization technique we also explicitly set which TVs to include using includeTVList.

depth

As another optimization technique we set the depth getResources should search, from our parents. A depth of 0 would mean only search direct children, 1 would go down to grandchildren and so on. Since we have three levels of depth (North America > West Coast > Alaska) we set our depth to 2 in the default Property Set.


Context Settings

North America (85) is the top most parent of Ski Resorts and Regions

In the SkiMap Chunk you may have noticed a [[++resorts_parents]] setting being passed into the [[$ListResorts]] Chunk. It looks like it uses the System Settings ++ Syntax, but there is no resorts_parent System Setting to be found. This is because it is in fact a Context Setting. Context Settings can be found by right clicking a context in the Resource Tree such as web and choosing Edit Context. Under the Context Settings tab we can manage settings that are specific to the given context. This settings will override System Settings with the same key.

There is a resorts_parents Context Setting used with a value of 85 to specify that the North America (85) resource is the ultimate parent of our ski resorts and regions.


Region Template

By cleverly using MODX Elements like Templates and Chunks we can keep from having to repeat ourselves. The Regions Template is a good example of this because it’s abstract enough to represent:

  • US States
  • Providences
  • Continents
  • National Regions

A Region is really just a defined area. Notice our Region Template has the following Template Variables:

  • gps.address
  • gps.doapi
  • gps.latitude
  • gps.longitude
  • mapzoom

A Region will always be a container, because it will always have Ski Resort children. That’s all for now. We’ll come back to the Region Template later on.


Property Sets

Instead of display the Resource content in the sidebar our Regions will list their child resorts. We’ll do this by using Property Sets to use the same [[$SkiMap]] chunk as we did before.

Ski Resort pages (left) display Resort Content while Region Pages (right) display pins

Go back to the [[$SkiMap]] and click the Properties tab. Let’s take a look at the Property Sets configured there. Wait, what are property sets?

MODX Property Sets are user-defined collections of properties for an Element. They can be attached to one, or more, Elements via that Element’s editing page. Those property sets, once attached, can then be called in their Element’s call syntax like such:
[[ElementName@PropertySetName]]

So Property Sets are just groups of Properties, that can be used on multiple elements. You can define as many properties as you’d like in Property Sets, and even easily override them.

Our Default SkiMap Property Set

Let’s review our properties here. For content, we simply are calling the pages content. You may be wondering what’s the point? Well this allows use to reference content using [[+content]] in our Chunk rather than directly calling [[*content]].

Referencing a Property
Hop back over to the Credit/edit chunk tab of the SkiMap and on line 29 replace [[*content]] with [[+content]]. Now instead of being “hard coded” to always being the current resource’s content, we are overriding this property to make it whatever we want.

Head back to the Properties tab and take a look at the Region Property Set. Notice the content and icon properties are green. This is indicating that they are overriding the default properties. For content we are simply calling the ListRegions Chunk and passing in the id of the current document and specifying another chunk to use for rendering individual items. This will print a list of all the Resorts in this Region. We also set our icon property to blank so no “ridden” icon is shown for Regions.

Overriding Default Properties

Now that may have seemed like a lot of effort to configure these property sets but it will be worth it. While you certainly could create an entire MODX site without using property sets at all, using property sets allows us to keep our code DRY (Don’t Repeat Yourself). So congrats, you’re one your way to being a fully certified DRY MODX coder.


Region Map

Remember how simple our Ski Resort Template was? The Region Property Set is going to allow us to keep our Region Template just as simple.

    [[$SkiMap@Region]]
   

We simply call our SkiMap Chunk but this time loading it with our Region Property Set. So remember this property set will:

  • list the Region’s Resorts instead of Resort content
  • Prevent an icon from displaying

Region Container

Nested Regions in the Resource Tree

You should see several Region Resources, such as Oregon, in your resource tree. Take a look at one of them in the MODX Manager so we can examine a few of its properties.

Region Properties

  • Title: Oregon
  • Template: Region
  • Address: Prineville, Or
  • Hit API: Yes
  • Map Zoom: 8
  • Container: Yes

By setting the Template of the Resource to Region, we are able to enter values for all of the Template Variables associated with the Region template, such as Address, Hit API and Map Zoom. Thanks to the addressGPS Extra, if Hit API is checked, when the resource is saved the latitude and longitude values will automatically be set to that of the Address.

When viewing the Oregon resource we see a map of Oregon’s Ski Resorts



Ski Resorts

Take a look at an existing Ski Resort and let’s examine the following properties.

Ski Resort in the Resource Tree


Resort Properties

  • Title: Timberline Lodge
  • Template: Ski Resort
  • Set Content
  • Address: 27500 Timberline W, Government Camp, OR
  • Hit API: Yes
  • Map Zoom: 10

Notice as far as MODX is concerned a Resort is almost the exact same thing as a Region. It’s a Resource with a unique template, some geolocation Template Variables and a Map Zoom setting.

Timberline Lodge



View Others

The view others link use the MODX URL Schemes to generate the link to the parent, and a simple parentName Snippet to fetch the parent name. The #sm hashtag added to the link will be used to tell our JavaScript to scroll past the map for mobile layouts.

<ul class="nav">
    <li style="width:100%;float:none;text-align:center"><a href="[[~[[*parent]]#sm]]">&ndash;&nbsp;&nbsp;View [[parentName]] Resorts&nbsp;&nbsp;&ndash;</a></li>
</ul>

This same view others code will work for multiple depths, allowing it to print lists for States like Oregon, Regions like Northwest or even continents like North America.


Sibling Nav

Previous & Next Links

The SiblingNav MODX Extra is used to easily add Previous and Next links above our View Others link. Take a look at line 27 of the [[$SkiMap]] Chunk and see all we need to do is call the SimpleNav Snippet and then reference two placeholders. Placeholders allow Snippets to store output in a global reference rather than directly returning it.

  

 


So far you’ve seen how to:

  • configure MODX Resources
  • assign Resources Templates and thus Template Variables
  • use getResources to output your markup as desired
  • use Context Settings to store settings
  • use PropertySets to keep your Chunks re-usable
  • create abstract container resources that can be nested
  • create a re-usable View Other In Navigation

Things are lookin’ up. Two more things to do before we write home:

  • speed the map up by using AJAX to load content rather than loading a new page
  • use getCache to cache our map data globally rather than per-resource

AJAX Forking

Currently clicking a map pin on the map will link to an entire other page. To speed things up, we’ll use AJAX to achieve the same experience (even the URL changing and history management) but without having to reload another page. You may have noticed the JavaScript is already set up to make it easy for us to enable AJAX for the map, but first we need to modify our Ski Resort template to allow us to specify whether it should output the entire page or just the part we need for the AJAX response.

Remember the part above about how we are going to contain everything within the SkiMap chunk because it will make it easier down the line? This is it :)
* MODX Revolution allows you to build your content and templating to your desired markup, not the other way around. Because of this there are an immeasurable number of ways to accomplish the the same task in MODX. This is just one of them.

This is one of my favorite ways to add AJAX support to a MODX site. I call it “AJAX Forking”. Rather than do something like route all our requests through a single AJAX resource that accepts an id, looks up and returns the desired content we will actually put the logic right in our Ski Resort template.

Currently our Ski Resort template says “run the SkiMap chunk”. That’s it. We want it to say “if an ajax URL parameter is detected, just run the ski-resort-ajax Chunk otherwise run the SkiMap chunk.” To accomplish this we will be using two MODX Extras:

As you may have gotten from their names, If will allow us to add conditional expressions to our templates and getUrlParam, well it’s a shortcut to retrieving URL parameters. So, let’s put it together.

[[!If?
&subject=`[[!getUrlParam? &name=`ajax` &int=`1`]]`
&operator=`==`
&operand=`1`
&then=`[[$ski-resort-ajax]]`
&else=`[[$SkiMap]]`
]]
 

If the ajax URL parameter has a value of 1, the ski-resort-ajax Chunk is returned. Otherwise the SkiMap chunk is. Awesome. There’s only one problem problem, we need to optimize this a little.

As Jason Coward explains in his Tags as the Result or How Conditionals are like Mosquitoes post, the MODX parser works from the inside out meaning in the above example both our then and else conditions are being processed. We don’t want that now do we! Luckily a very simple trick and will have us on our way. Remove the square brackets from the then and else conditions making them $ski-resort-ajax and $SkiMap respectively. Now add double square brackets surrounding the If conditional leaving us with, drum-roll please.

[[[[!If?
&subject=`[[!getUrlParam? &name=`ajax` &int=`1`]]`
&operator=`==`
&operand=`1`
&then=`$ski-resort-ajax`
&else=`$SkiMap`
]]]]
 

What the what? I know it looks kinda weird, but let’s walk through what’s going on. If will evaluate the result of the getUrlParam request and either print $ski-resort-ajax or $SkiMap. Since doubled up on our brackets, that will result in [[$ski-resort-ajax]] or [[$SkiMap]]. Then and only then will the desired Chunk be processed. Voola!

Use ClientConfig to easily manage settings


Ok last step is to enable the AJAX loading. Under the Components menu click Configurations. In the Map tab, check the Map AJAX setting and then Save Configuration. Now map pins load using AJAX!


getCache

getCache is a MODX Extra that allows you to cache your MODX Elements to the filesystem. You can do things like set how long they should be cached for, or even share the cache file across multiple resources. This can be used for all sorts of optimizations but what we will be using it for is sharing that mega huge list of all our map data globally. This will make our pages load much quicker.

Hop back over to the List Resorts Chunk which currently looks like this:

[[getResources?
&parents=`[[+parent]]`
&where=`{"template:=":[[+resort_template]]}`
&sortby=`[[+sortby]]`
&tpl=`[[+tpl]]`
&limit=`0`
&includeTVs=`1`
&includeTVs=`1`
&includeTVList=`gps.latitude,gps.longitude,ridden`
&depth=`[[+depth]]`
]]

We are going to:

  • wrap getResources in getCache
  • set some properties to configure getCache
[[!getCache?
&element=`getResources`
&cacheElementKey=`map-pins`
&cacheExpires=`0`
&parents=`[[+parent]]`
&where=`{"template:=":[[+resort_template]]}`
&sortby=`[[+sortby]]`
&tpl=`[[+tpl]]`
&limit=`0`
&includeTVs=`1`
&depth=`[[+depth]]`
&includeTVs=`1`
]]

You know the drill, let’s explain. Notice we are now calling the getCache Snippet uncached, rather than the getResources tag cached. getCache always needs to be called uncached so it can do its magic.

element

This is how we tell getCache what MODX Element to process (snippet or chunk). To use chunks you must set elementClass to modChunk.

cacheElementKey

Using this property we tell getCache to store this cache with an id of map-pins. A shared cache file will be generated for map pins that each resource will reference. This reduces the number of times List Resorts is processed after the Resource Cache has been cleared from the number of pages on the site to 1. So pretty big difference in performance just with cleverly making use of one parameter!

cacheExpires

This is the amount of time we are telling getCache to cache the output. A value of 0 means let it live forever. Since we haven’t explicitly told getCache to store cache files elsewhere it will store them in the Resource Cache. The Resource Cache gets cleared whenever we make changes to resources, so we don’t have to worry about this getting out of date even with a cacheExpires time of forever.

Using cacheKey you could tell getCache to store the output outside of the resource_cache area, so that the results aren’t cleared automatically after making publishing updates. See the Refresh Custom Cache Partitions page of the getCache wiki for more details on how to add a menu item for clearing custom cache areas.

Comments (7)

  1. The other @Crssp:
    Apr 17, 2013 at 09:14 AM

    Z'up homie Jeffries.
    This is on awesome tutorial.
    The lazy man's way for all of us, would be if you were to make a Site package of some sort to start to build on, lol.
    Seriously awesome thanks again!

  2. JP DeVries:
    Apr 17, 2013 at 11:25 AM

    Glad you found it useful. I'm working on packaging this tutorial using MyComponent, and it's probably about 85% complete. Still need to figure out packaging in the Property Sets and confirm all the elements and System Settings make it through.

  3. Dan:
    Apr 17, 2013 at 05:18 PM

    This is a fantastic tutorial. I was just thinking about a map based MODX project and your code has given me many ideas to think about from where to start to the paths I could take it down. Thanks!

  4. Crssp:
    Apr 18, 2013 at 01:40 PM

    How would you go about setting this up on a local test server, or a solution without the Cloud snapshot? That part didn't jump out at me before.
    What exactly is Vapor?
    Thanks for more infos.

  5. JP DeVries:
    Apr 18, 2013 at 01:43 PM

    Crssp,

    Vapor is a way to get sites into MODX Cloud. I'm still working on packing this tutorial as a standard MODX package using MyComponent. When complete you'll be able to install it on any MODX site, although you will want to do so on a "blank" installation as it will do things like modify your resource tree.

  6. Ben Davis:
    May 25, 2013 at 06:48 AM

    Hey JP,
    Looks like a great tutorial here. I will absolutely be going through this. Cool that you are using siblingNav! That is an add-on that Bruno and I put together.

    Keep up the good work!

  7. Crssp:
    May 31, 2013 at 10:11 AM

    Hey curious jp, when developing this, did you take a look at the store locator module?

    Would that have been beneficial, just noticed it at:
    http://rtfm.modx.com/display/ADDON/StoreLocator
    Just wondered how this compares to your setup, ifyou looked at it?


Add a Comment





Allowed tags: <b><i><br>Comment:


** Go ski somewhere **

modmore