Openlayers POI layer example 2

From OpenStreetMap Wiki
Jump to: navigation, search

Purpose

Screenshot of the result

I have expanded the Openlayers_POI_layer_example a little to make several layers for different classes of POI. This something that is useful for me in my town, and may also be useful to others.

The previous example shows how to use OpenLayers to add a marker layer on top of an OSM slippy map. My example goes one step further and automatically extracts the coordinates for POIs from the OSM map XML data.

Here's a brief summary of what it does.

  • Download OSM XML data for a portion of the map, e.g. the area around a town.
  • Use XSLT to extract certain POIs from the XML
  • Build a POI HTML file which centres the map on the town and adds the POIs as layers

This is done with a combination of Python and XML tools.

Requirements

In addition to the Python script and XSLT file you will need:

  • Python
  • libxml2
  • libxslt
  • icons for your POIs (There are some nice ones here)
  • a webserver to put this stuff

The XSLT will cope with POIs which are nodes, and POIs which are areas. In the case of nodes, the lat/lon of the node is used as the lat/lon for the POI icon. In the case of areas, a simple average of lat/lon of the points enclosing the area is used. This is not perfect, but it's easy to do and adequate.

You will need to edit the lat/lon coordinates for the map region. They are marked edit_me in the code.

The files

extract_amenities.py

#! /usr/bin/python
# -*- coding: UTF-8 -*-

import libxml2
import libxslt

import urllib
import os

# Program to produce amenity layers for OpenLayers
# * Download data from OSM
# * Use XSLT to extract lat/lon pairs for each type of amenity
# * Build the poi.html framework for the layers

# Released for OSM under CC-by-SA licence

def main():
  
  libxml2.lineNumbersDefault(1)
  libxml2.substituteEntitiesDefault(1)
  
  # The extent of the data area to download from OSM
  min_lon=edit_me
  min_lat=edit_me
  max_lon=edit_me
  max_lat=edit_me
  
  # The initial zoom level for the POI html file.
  initial_zoom = 14

  # URL to fetch the OSM data from
  map_source_data_url="http://www.informationfreeway.org/api/0.6/map?bbox=%s,%s,%s,%s"\
  %(min_lon,min_lat,max_lon,max_lat)

  # Filename for OSM map data
  xml_filename = "map.osm"
  
  # Filename for XSLT to extract POIs
  xsl_filename = "amenities.xsl"

  # Filename for POI html file output
  poi_html_filename = "poi.html"

  # Layers we are going to extract (in a dict)
  # The key is the layer name, the value is a list of parameters:
  # 0,1: OSM key, value
  # 2: POI text output file name
  # 3: icon for this type of POI
  # 4,5: icon width,height (px)
  # 6,7: icon offset (x,y) (px)
  marker_layers={
    "Convenience stores":["shop", "convenience", "conbiniis.txt", "conbinii.png",32,37,-16,-37],
    "Hagwons":["amenity", "hagwon", "hagwons.txt", "hagwon.png",32,37,-16,-37],
    "Libraries":["amenity", "library", "libraries.txt", "library.png",32,37,-16,-37],
    "Post Offices":["amenity", "post_office", "postoffices.txt", "postoffice.png",32,37,-16,-37],
    "Schools":["amenity", "school", "schools.txt", "school.png",32,37,-16,-37],
    "Supermarkets":["shop", "supermarket", "supermarkets.txt", "supermarket.png",32,37,-16,-37],
     }

  # Calculate the centre of the map extent
  map_centre_lon = (min_lon + max_lon)/2 
  map_centre_lat = (min_lat + max_lat)/2 

  # Download the map.osm file from the net, if we don't already have one.
  if os.path.isfile(xml_filename):
    print "Not downloading map data.  '%s' already exists."%xml_filename
  else:
    print "Downloading OSM data."
    print "'%s' -> '%s'"%(map_source_data_url,xml_filename)
    urllib.urlretrieve(map_source_data_url,xml_filename)

  # Read the XML into memory.  We will use it many times.
  osmdoc = libxml2.parseFile(xml_filename)

  # Read the XSLT
  styledoc = libxml2.parseFile(xsl_filename)
  style = libxslt.parseStylesheetDoc(styledoc)

  # Extract POIs to layer text files
  for layer,tags in marker_layers.iteritems():
    print "Extracting '%s'..."%layer
    layer_filename = tags[2]
    result = style.applyStylesheet(osmdoc,\
    { "key":"'%s'"%tags[0], "value":"'%s'"%tags[1], "icon":"'%s'"%tags[3],\
    "width":"'%s'"%tags[4], "height":"'%s'"%tags[5],\
    "offsetx":"'%s'"%tags[6], "offsety":"'%s'"%tags[7]
    })
    style.saveResultToFilename(layer_filename, result, 0)

# Putting the HTML in the code is bad form, but it can be put somewhere else later
  poi_html_top='''<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <style type="text/css">
#map {
        width: 100%;
        height: 100%;
        border: 0px;
        padding: 0px;
        position: absolute;
     }
body {
        border: 0px;
        margin: 0px;
        padding: 0px;
        height: 100%;
     }
    </style>
    <script src="http://www.openlayers.org/api/OpenLayers.js"></script>
    <script src="http://www.openstreetmap.org/openlayers/OpenStreetMap.js"></script>
    <script type="text/javascript">
        <!--
        var map;
 
        function init(){
            map = new OpenLayers.Map('map',
                    { maxExtent: new OpenLayers.Bounds(-20037508.34,-20037508.34,20037508.34,20037508.34),
                      numZoomLevels: 19,
                      maxResolution: 156543.0399,
                      units: 'm',
                      projection: new OpenLayers.Projection("EPSG:900913"),
                      displayProjection: new OpenLayers.Projection("EPSG:4326")
                    });
 
            var layerMapnik = new OpenLayers.Layer.OSM.Mapnik("Mapnik");
 

            map.addLayers([layerMapnik,layerTah]);
'''

  poi_html_bottom=''' 
            map.addControl(new OpenLayers.Control.LayerSwitcher());
 
            var lonLat = new OpenLayers.LonLat(%s, %s).transform(map.displayProjection,  map.projection);
            if (!map.getCenter()) map.setCenter (lonLat, %s);
        }
        // -->
    </script>
  </head>
  <body onload="init()">
    <div id="map"></div>
  </body>
</html>'''%(map_centre_lon,map_centre_lat,initial_zoom)

  print "Writing '%s'..."%poi_html_filename
  f = open(poi_html_filename,"w")
  
  # Write out the POI html file.
  # This could be index.html if you like.
  f.write(poi_html_top)
  
  # Add the POI layers.  Make them all invisible initially.
  # Don't forget, dict entries are not ordered.  The list
  # of layers will appear in an inconsistent order.
  for layer,tags in marker_layers.iteritems():
    f.write ('''var pois = new OpenLayers.Layer.Text( "%s",
                    { location:"./%s",
                      projection: map.displayProjection,
                      visibility: 0
                    });
            map.addLayer(pois);'''%(layer,tags[2]))
  
  f.write(poi_html_bottom)
  f.close()

if __name__=="__main__":
  main()

amenities.xsl

Originally this file handled only amenity=*, but you can see the that key and value to search for are passed as parameters.

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE xsl:stylesheet [
<!ENTITY tab '<xsl:text xmlns:xsl="http://www.w3.org/1999/XSL/Transform">&#9;</xsl:text>'>
<!ENTITY cr '<xsl:text xmlns:xsl="http://www.w3.org/1999/XSL/Transform">&#13;</xsl:text>'>
<!ENTITY quot '<xsl:text xmlns:xsl="http://www.w3.org/1999/XSL/Transform">&#34;</xsl:text>'>
]>

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

<!-- XSLT to extract amenities from OSM data.
Takes seven parameters and returns a tab-delimited text file suitable
for use with OpenLayers.

key: The osm key to match, e.g. 'amenity'
value: The value of the key for the items that are to be extracted, e.g. 'library'
icon: The filename of the icon to use for this point
width,height: The width and height of the icon
offsetx,offsety: The icon offset

The first two parameters are used to extract objects from the OSM XML.
The last five parameters are written verbatim to the columns in the
output text file.

Objects which are nodes are returned as the single lat/lon pair
for each node.
Objects which are areas are returned as a single lat/lon pair for
the centroid of the area.  The centroid is a cheap average, which is
adequate for this purpose.

The text in the popup box for each icon is simply the name=* tag for
the object, and a line of text which reads 'node' or 'area' depending
on how the object's lat/lon was extracted.
-->

<xsl:output method="text"/>

<xsl:param name="key">None</xsl:param>
<xsl:param name="value">None</xsl:param>
<xsl:param name="icon">None</xsl:param>
<xsl:param name="width">None</xsl:param>
<xsl:param name="height">None</xsl:param>
<xsl:param name="offsetx">None</xsl:param>
<xsl:param name="offsety">None</xsl:param>

<xsl:template match="osm">lat&tab;lon&tab;title&tab;description&tab;icon&tab;iconSize&tab;iconOffset
<xsl:apply-templates select="node"/>
<xsl:apply-templates select="way"/>
</xsl:template>

<xsl:template match="node">
<xsl:for-each select="tag">
<xsl:if test='@k=$key and @v=$value'>
<xsl:value-of select='../@lat'/>&tab;
<xsl:value-of select='../@lon'/>&tab;
<xsl:value-of select='../tag[@k="name"]/@v'/>&tab;Node&tab;
<xsl:value-of select='$icon'/>&tab;
<xsl:value-of select='$width'/>,<xsl:value-of select='$height'/>&tab;
<xsl:value-of select='$offsetx'/>,<xsl:value-of select='$offsety'/>&cr;
</xsl:if>
</xsl:for-each>
</xsl:template>

<xsl:template match="way">
<xsl:variable name="noderefs" select="nd[not(@ref=preceding-sibling::nd/@ref)]"/>
<xsl:variable name="nodes" select="../node[@id=$noderefs/@ref]"/>
<xsl:for-each select="tag">
<xsl:if test='@k=$key and @v=$value'>
<xsl:value-of select="sum($nodes/@lat) div count($nodes)"/>&tab;
<xsl:value-of select="sum($nodes/@lon) div count($nodes)"/>&tab;
<xsl:value-of select='../tag[@k="name"]/@v'/>&tab;Area&tab;
<xsl:value-of select='$icon'/>&tab;
<xsl:value-of select='$width'/>,<xsl:value-of select='$height'/>&tab;
<xsl:value-of select='$offsetx'/>,<xsl:value-of select='$offsety'/>&cr;
</xsl:if>
</xsl:for-each>
</xsl:template>

</xsl:stylesheet>

Output

The output files will depend on the configuration of the script. The map.osm file is not really an output, but it is a side-effect of running the program.

  • poi.html
    The main file which loads the map and layers
  • amenity.txt
    The POI file for the class of amenity (e.g. supermarkets.txt)

You should upload these files to your webserver, along with the icons that you have specified.

See also