Skip to content

Instantly share code, notes, and snippets.

@ryanj
Last active December 25, 2015 17:59
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ryanj/7017208 to your computer and use it in GitHub Desktop.
Save ryanj/7017208 to your computer and use it in GitHub Desktop.
Spatial mapping with PHP, Silex, MongoDB, Leaflet, and Open Street Maps.

Open Source Mapping with PHP and MongoDB

In response to Steve's call for a shift toward civic-focused applications that are a better suited for adoption and maintenance by PHP-friendly IT departments, I've momentarily set aside OpenShift's shiny new nodejs-0.10 cartridge in order to show you how easy it is to write a basic single-page mapping application in PHP.

Those of you who are familiar with Steve's mapping quickstarts may recognize this application as a port of his own MongoDB-Python-Flask example which uses MongoDB's spatial features to perform a classic bounding-box query.

Project source: https://github.com/openshift-quickstart/silex-mongodb-parks

Whether your goals are civic-minded or otherwise, PHP can help you craft solutions that are every bit as simple and elegant as what you might expect to see when using modern Python, Ruby, or JavaScript frameworks. This particular example is intended to serve as a reusable template for folks who are interested in producing their own mapping applications - substituting in their own collection of map points or other spatial data.

The included sample data contains the names and locations of major National Parks and Historic Sites.

The Datastore: MongoDB

MongoDB provides basic support for spatial data and queries, so I've chosen to use it over MySQL in this example. Since this demo currently only includes a simple bounding box selection, it might be possible to write it with MySQL in mind. But, the lack of real support for geospatial extensions might eventually become a problem if mapping is a central theme in your application.

Our new postgresql-9.2 cartridge includes support for PostGIS 2.1, which provides the most advanced collection of geo-spatial features available on OpenShift. Ultimately, choosing the right tool for the job may involve finding solutions that help to bridge technology communities, requiring minimal long-term effort to clone, configure, host, and maintain.

MongoDB also provides support for flexible Schemaless data structures (NoSQL), making it an ideal candidate for early projects and evolving prototypes, or for applications with plenty of sparse data or unstructured information.

When importing data, MongoDB expects it's input files to list a single JSON document per line. Our project's parkcoord.json file conforms to these conditions, providing an easy way to load our sample data:

{
  "Name" : "Abraham Lincoln Birthplace National Historical Park", 
  "pos"  : [-85.7302 , 37.5332 ] 
}

Bootstrapping your Database

If included in your project, the .openshift/action_hooks/deploy script will be executed just before your application is started. A list of the available build scripts or action_hooks can be found in our guide for deploying and building applications.

Our deploy script is configured to automatically bootstrap our MongoDB database with the following command:

mongoimport --db $OPENSHIFT_APP_NAME --collection parks --host $OPENSHIFT_MONGODB_DB_HOST --username $OPENSHIFT_MONGODB_DB_USERNAME --password $OPENSHIFT_MONGODB_DB_PASSWORD --port $OPENSHIFT_MONGODB_DB_PORT --type json --file $OPENSHIFT_REPO_DIR/parkcoord.json

It will then make a call to php/bootstrap.php, adding a spatial index to our data collection:

$parks->ensureIndex(array('pos'=>"2d"));

Now that our data has been imported and indexed, let's take a look at our Back-end API and set up our map's bounding-box query.

The Back-End: Silex

Silex is a clean and simple microFramework built from reusable Symfony2 components. It provides an interface reminiscent of Sinatra, Flask, or RESTify, and can be installed via PHP's Composer dependency manager.

Composer

The example project includes a composer.phar file that can be used to help manage dependencies. Dependencies are recorded in composer.json as well as in the associated composer.lock file.

{
    "require": {
        "silex/silex": "~1.1"
    }
}

Composer will install all it's dependencies into your project's vendor folder.

Inside your project's php folder, an .htaccess file will redirect all web traffic requests to our main app.php file.

The Silex controller code is simple to load:

require '../vendor/autoload.php';
$app = new \Silex\Application();

In app.php we include a basic route for our index page:

$app->get('/', function () use ($app) {
  return $app->sendFile('../static/index.html');
});

And, a route for the static assets in our css folder:

$app->get('/css/{filename}', function ($filename) use ($app){
  if (!file_exists('../static/css/' . $filename)) {
    $app->abort(404);
  }
  return $app->sendFile('../static/css/' . $filename, 200, array('Content-Type' => 'text/css'));
});

Our Back-end API includes a /parks/within endpoint, which contacts MongoDB to execute our bounding-box query, producing a JSON data response that will be used to update our map view:

$app->get('/parks/within', function () use ($app) {
  $db_name = getenv('OPENSHIFT_APP_NAME') ? getenv('OPENSHIFT_APP_NAME') : 'parks';
  $db_connection = getenv('OPENSHIFT_MONGODB_DB_URL') ? getenv('OPENSHIFT_MONGODB_DB_URL') . $db_name : "mongodb://localhost:27017/" . $db_name;
  $client = new MongoClient($db_connection);
  $db = $client->selectDB($db_name);
  $parks = new MongoCollection($db, 'parks');

  $bounding_box = array( 'pos' => 
                  array( '$within' => 
                  array( '$box' =>
                  array(  array( $lon1, $lat1),
                          array( $lon2, $lat2)))));

  $result = $parks->find( $bounding_box );

  $response = "[";
  foreach ($result as $park){
    $response .= json_encode($park);
    if( $result->hasNext()){ $response .= ","; }
  }
  $response .= "]";

  return $app->json(json_decode($response));
});

On OpenShift, your Mongodb connection details will be automatically provided to your app via system environment variables, allowing you to keep your code clean and free of passwords, keys, and other configuration strings. The above code checks to see if these variables are populated, providing sensible fallback values to help support local development environments.

The server-side code concludes with an $app->run(); statement.

The Front-End: Leaflet

All of the View-related code is written in Javascript and is run on the client-side.

Leaflet's mapping solution is well documented, looks great, and is really easy to setup and configure.

Include a link to Leaflet's css stylesheet in our index.html file:

<link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.5.1/leaflet.css" />

We'll also need to initialize our map view, and include a few event hooks and callbacks for updating our map content whenever our viewport is being loaded or modified:

<script src="http://cdn.leafletjs.com/leaflet-0.5.1/leaflet.js"></script>
<script>
  var map = L.map('map').setView([42.35, -71.06], 12);
  var markerLayerGroup = L.layerGroup().addTo(map);
  L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
    maxZoom: 18,
    attribution: 'Map data &copy; <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>'
  }).addTo(map);

  function getPins(e){
    bounds = map.getBounds();
    url = "parks/within?lat1=" + bounds.getSouthWest().lat + "&lon1=" + bounds.getSouthWest().lng + "&lat2=" + bounds.getNorthEast().lat + "&lon2=" + bounds.getNorthEast().lng;
    $.get(url, pinTheMap, "json")
  }

  function pinTheMap(data){
    //clear the current map pins
    map.removeLayer(markerLayerGroup);

    //add the new pins
    var markerArray = new Array(data.length)
    for (var i = 0; i < data.length; i++){
      park = data[i];
      markerArray[i] = L.marker([park.pos[1], park.pos[0]]).bindPopup(park.name);
    }
    markerLayerGroup = L.layerGroup(markerArray).addTo(map);
  }

  // update the map pins whenever the map is redrawn:
  map.on('dragend', getPins);
  map.on('zoomend', getPins);
  map.whenReady(getPins)
</script>

The dragend and zoomend map events will now fire whenever the map viewport is adjusted.

Writing Simple, Clean, Reusable Code

I know I've used MongoDB for this demo, which may not be the easiest DB solution for older IT departments to adopt. However, OpenShift really helps to address that concern by providing an open platform that is readily available for use within any IT department - ensuring consistent, reliable, and supported access to newer technologies.

This means that you can build reusable applications that are completely portable across open cloud platforms, an ideal scenario for open civic-focused community solutions.

To deploy a clone of this application using the rhc command line tool:

rhc app create parks php-5.3 mongodb-2.2 --from-code=https://github.com/ryanj/silex-mongodb-parks.git

Or clone+deploy via our web-based workflow in a single click:

https://openshift.redhat.com/app/console/application_type/custom?cartridges%5B%5D=php-5.3&cartridges%5B%5D=mongodb-2.2&initial_git_url=https%3A%2F%2Fgithub.com%2Fryanj%2Fsilex-mongodb-parks.git

A live demo is available here: http://phparks-shifter.rhcloud.com/

What's Next?

@duythien
Copy link

duythien commented Jan 2, 2015

Nice article, so what's next ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment