Tuesday 31 March 2015

MapGuide 3.0 feature showcase: Tile Service Enhancements

If I were to pick the most compelling feature of MapGuide Open Source 3.0, it would be the one I am about to show you in this post.

Do you bemoan the fact that tiled maps are tied to a given Map Definition preventing their re-use with other Map Definitions?

There are ways to work around this problem, by using the "hack" of stacking multiple Map Definitions in a Fusion Application Definition. In this case, you would have a purely tiled Map Definition and you combine this with a purely dynamic Map Definition in your Application Definition and provided both maps are in the same coordinate system, both tiled and dynamic layers will line up.

You can then reuse this tiled map definition with other Fusion applications by mixing and matching tiled and dynamic Map Definitions together. However as already mentioned, this is a hack because it exploits an esoteric feature of Fusion: The ability to have more than one Map Definition specified in a Fusion MapGroup. This technique doesn't work with the AJAX viewer.

For the 3.0 release, we have solved this problem by introducing a new resource type to define shareable tile sets: The TileSetDefintion.

For a cliff notes summary: A TileSetDefinition is a re-usable tile cache that is effectively the BaseMapDefinition element of the Map Definition Schema separated out into its own resource. A TileSetDefinition lets you:
  • Define one or more base layer groups, each with 0 or more layers
  • Define a finite set of scales
  • Define the coordinate system of this tile set
  • Define the bounds of this tile set
Basically the same tiled layer settings you would've previously defined in a Map Definition. In addition a TileSetDefinition also lets you define additional settings that are specific to that tile set:
  • Tile size
  • Tile image format
  • Tile storage location
No longer are your tile sets constrained by global tile settings in serverconfig.ini and no longer are your rendered tiles always stored in a global defined tile cache directory. Each tile set can have their own tile size, format, storage location.

Here's an example of such a Tile Set Definition, notice the structural similarity to the Base Map section of the Map Definition

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
<?xml version="1.0" encoding="utf-8"?>
<TileSetDefinition xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xsi:noNamespaceSchemaLocation="TileSetDefinition-3.0.0.xsd">
  <TileStoreParameters>
    <TileProvider>Default</TileProvider>
    <Parameter>
      <Name>TilePath</Name>
      <Value>%MG_TILE_CACHE_PATH%</Value>
    </Parameter>
    <Parameter>
      <Name>TileWidth</Name>
      <Value>256</Value>
    </Parameter>
    <Parameter>
      <Name>TileHeight</Name>
      <Value>256</Value>
    </Parameter>
    <Parameter>
      <Name>TileFormat</Name>
      <Value>PNG</Value>
    </Parameter>
    <Parameter>
      <Name>FiniteScaleList</Name>
      <Value>200000,100000,50000,25000,12500,6250,3125,1562.5,781.25,390.625</Value>
    </Parameter>
    <Parameter>
      <Name>CoordinateSystem</Name>
      <Value>GEOGCS["LL84",DATUM["WGS84",SPHEROID["WGS84",6378137.000,298.25722293]],PRIMEM["Greenwich",0],UNIT["Degree",0.01745329251994]]</Value>
    </Parameter>
  </TileStoreParameters>
  <Extents>
    <MinX>-87.764986990962839</MinX>
    <MaxX>-87.695521510899724</MaxX>
    <MinY>43.691398128787782</MinY>
    <MaxY>43.797520000480347</MaxY>
  </Extents>
  <BaseMapLayerGroup>
    <Name>Base Layer Group</Name>
    <Visible>true</Visible>
    <ShowInLegend>true</ShowInLegend>
    <ExpandInLegend>true</ExpandInLegend>
    <LegendLabel>Tiled Layers</LegendLabel>
    <BaseMapLayer>
      <Name>Roads</Name>
      <ResourceId>Library://Samples/Sheboygan/Layers/Roads.LayerDefinition</ResourceId>
      <Selectable>false</Selectable>
      <ShowInLegend>true</ShowInLegend>
      <LegendLabel>Roads</LegendLabel>
      <ExpandInLegend>true</ExpandInLegend>
    </BaseMapLayer>
    <BaseMapLayer>
      <Name>Districts</Name>
      <ResourceId>Library://Samples/Sheboygan/Layers/Districts.LayerDefinition</ResourceId>
      <Selectable>false</Selectable>
      <ShowInLegend>true</ShowInLegend>
      <LegendLabel>Districts</LegendLabel>
      <ExpandInLegend>true</ExpandInLegend>
    </BaseMapLayer>
    <BaseMapLayer>
      <Name>Buildings</Name>
      <ResourceId>Library://Samples/Sheboygan/Layers/Buildings.LayerDefinition</ResourceId>
      <Selectable>false</Selectable>
      <ShowInLegend>true</ShowInLegend>
      <LegendLabel>Buildings</LegendLabel>
      <ExpandInLegend>true</ExpandInLegend>
    </BaseMapLayer>
    <BaseMapLayer>
      <Name>Parcels</Name>
      <ResourceId>Library://Samples/Sheboygan/Layers/Parcels.LayerDefinition</ResourceId>
      <Selectable>true</Selectable>
      <ShowInLegend>true</ShowInLegend>
      <LegendLabel>Parcels</LegendLabel>
      <ExpandInLegend>true</ExpandInLegend>
    </BaseMapLayer>
    <BaseMapLayer>
      <Name>Islands</Name>
      <ResourceId>Library://Samples/Sheboygan/Layers/Islands.LayerDefinition</ResourceId>
      <Selectable>false</Selectable>
      <ShowInLegend>true</ShowInLegend>
      <LegendLabel>Islands</LegendLabel>
      <ExpandInLegend>true</ExpandInLegend>
    </BaseMapLayer>
    <BaseMapLayer>
      <Name>Hydrography</Name>
      <ResourceId>Library://Samples/Sheboygan/Layers/Hydrography.LayerDefinition</ResourceId>
      <Selectable>false</Selectable>
      <ShowInLegend>true</ShowInLegend>
      <LegendLabel>Hydrography</LegendLabel>
      <ExpandInLegend>true</ExpandInLegend>
    </BaseMapLayer>
    <BaseMapLayer>
      <Name>CityLimits</Name>
      <ResourceId>Library://Samples/Sheboygan/Layers/CityLimits.LayerDefinition</ResourceId>
      <Selectable>false</Selectable>
      <ShowInLegend>true</ShowInLegend>
      <LegendLabel>CityLimits</LegendLabel>
      <ExpandInLegend>true</ExpandInLegend>
    </BaseMapLayer>
  </BaseMapLayerGroup>
</TileSetDefinition>

Finally, there's the re-usability aspect. Once a TileSetDefinition has been created, you can link to it from any Map Definition. Tile caches are now bound to the TileSetDefinition instead of the Map Definition. When a Map Definition links to a TileSetDefinition, it will override the following settings from the Map Definition:
  • Coordinate System
  • Map Extents
The reason we do this is because dynamic map layers can be re-projected, but tiles cannot. So in the event that the Map Definition has a different coordinate system from the one in the linked tile set, the linked tile set "wins" and all the dynamic layers in the Map Definition will be re-projected to the coordinate system of the tile set, ensuring that both dynamic and tiled layers will line up properly.

And the best thing about this is that both AJAX and Fusion viewers support shareable tile sets*. The above multiple map hack is no longer needed.

* For this release, this will only work with shareable tile sets using the "Default" tile provider. Why? Read on below.

XYZ Tile Support

The TileSetDefinition schema allows tile set settings to be defined in a key/value fashion like you would in a Feature Source. This schema design is intentional as it allows us to define a FDO-style provider model behind the scenes for tile access. Through this provider model, we are able to add support for different tile access schemes, one of which is XYZ tiles. This means you no longer need mapguide-rest to serve XYZ tiles. MapGuide 3.0 will have this support natively.

Here's an example of a Tile Set Definition using the XYZ tile provider

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
<?xml version="1.0" encoding="utf-8"?>
<TileSetDefinition xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xsi:noNamespaceSchemaLocation="TileSetDefinition-3.0.0.xsd">
  <TileStoreParameters>
    <TileProvider>XYZ</TileProvider>
    <Parameter>
      <Name>TilePath</Name>
      <Value>%MG_TILE_CACHE_PATH%</Value>
    </Parameter>
    <Parameter>
      <Name>TileFormat</Name>
      <Value>PNG</Value>
    </Parameter>
  </TileStoreParameters>
  <Extents>
    <MinX>-9770571.93250815</MinX>
    <MaxX>-9755615.48593707</MaxX>
    <MinY>5416573.69002144</MinY>
    <MaxY>5436091.17493748</MaxY>
  </Extents>
  <BaseMapLayerGroup>
    <Name>Base Layer Group</Name>
    <Visible>true</Visible>
    <ShowInLegend>true</ShowInLegend>
    <ExpandInLegend>true</ExpandInLegend>
    <LegendLabel>Tiled Layers</LegendLabel>
    <BaseMapLayer>
      <Name>Roads</Name>
      <ResourceId>Library://Samples/Sheboygan/Layers/Roads.LayerDefinition</ResourceId>
      <Selectable>false</Selectable>
      <ShowInLegend>true</ShowInLegend>
      <LegendLabel>Roads</LegendLabel>
      <ExpandInLegend>true</ExpandInLegend>
    </BaseMapLayer>
    <BaseMapLayer>
      <Name>Districts</Name>
      <ResourceId>Library://Samples/Sheboygan/Layers/Districts.LayerDefinition</ResourceId>
      <Selectable>false</Selectable>
      <ShowInLegend>true</ShowInLegend>
      <LegendLabel>Districts</LegendLabel>
      <ExpandInLegend>true</ExpandInLegend>
    </BaseMapLayer>
    <BaseMapLayer>
      <Name>Buildings</Name>
      <ResourceId>Library://Samples/Sheboygan/Layers/Buildings.LayerDefinition</ResourceId>
      <Selectable>false</Selectable>
      <ShowInLegend>true</ShowInLegend>
      <LegendLabel>Buildings</LegendLabel>
      <ExpandInLegend>true</ExpandInLegend>
    </BaseMapLayer>
    <BaseMapLayer>
      <Name>Parcels</Name>
      <ResourceId>Library://Samples/Sheboygan/Layers/Parcels.LayerDefinition</ResourceId>
      <Selectable>true</Selectable>
      <ShowInLegend>true</ShowInLegend>
      <LegendLabel>Parcels</LegendLabel>
      <ExpandInLegend>true</ExpandInLegend>
    </BaseMapLayer>
    <BaseMapLayer>
      <Name>Islands</Name>
      <ResourceId>Library://Samples/Sheboygan/Layers/Islands.LayerDefinition</ResourceId>
      <Selectable>false</Selectable>
      <ShowInLegend>true</ShowInLegend>
      <LegendLabel>Islands</LegendLabel>
      <ExpandInLegend>true</ExpandInLegend>
    </BaseMapLayer>
    <BaseMapLayer>
      <Name>Hydrography</Name>
      <ResourceId>Library://Samples/Sheboygan/Layers/Hydrography.LayerDefinition</ResourceId>
      <Selectable>false</Selectable>
      <ShowInLegend>true</ShowInLegend>
      <LegendLabel>Hydrography</LegendLabel>
      <ExpandInLegend>true</ExpandInLegend>
    </BaseMapLayer>
    <BaseMapLayer>
      <Name>CityLimits</Name>
      <ResourceId>Library://Samples/Sheboygan/Layers/CityLimits.LayerDefinition</ResourceId>
      <Selectable>false</Selectable>
      <ShowInLegend>true</ShowInLegend>
      <LegendLabel>CityLimits</LegendLabel>
      <ExpandInLegend>true</ExpandInLegend>
    </BaseMapLayer>
  </BaseMapLayerGroup>
</TileSetDefinition>

Notice that the XYZ tile set definition does not specify a coordinate system. This is because XYZ tile sets are always in WGS84.PseudoMercator (aka. EPSG:3857), so there is no need to specify this. The extents describe the bounds of this tile set in EPSG:3857 coordinates.

You'll notice in both examples, that we have the concept of a tile provider along with a set of key/value pairs to describe settings applicable for that provider, which is similar to the how a Feature Source specifies a FDO provider and a set of key/value pairs that form the set of FDO connection properties.

And what is this %MG_TILE_CACHE_PATH%? It is a placeholder for the global server tile cache path, and just like %MG_DATA_FILE_PATH% for feature sources, you can replace this with actual physical paths or MapGuide aliases. That's right, your Tile Set Definitions can have their own defined tile storage location instead of being all under a globally defined path in serverconfig.ini

As you can see, the similarity between tile providers and FDO was quite intentional :)

This provider model is currently hard-coded for these 2 providers. There is no formal plugin model/API yet that allows for external third-party tile providers to be defined. Whether we will add such support in a future release is a discussion for another time.

The best thing about this XYZ support: No API changes required! You still use the same GETTILE requests to the mapagent, only now you pass in the actual X, Y and Z for the ROW, COL and SCALE parameters.

Now there is one limitation of the XYZ support for this release. You cannot create MgMap instances from a Map Definition that links to an XYZ tile set, and as a result you cannot load such Map Definitions in the AJAX and Fusion viewers. The main use case for XYZ tile support in this release is to enable easy consumption of XYZ tile sets in MapGuide by external clients like OpenLayers, Leaflet, etc. We'll correct this limitation for the next major release after 3.0.

With this release, there's a new sample you can check out that demonstrate how to consume XYZ tiles from OpenLayers.


When you look at the code, you'll find consuming XYZ tile sets from MapGuide is as simple as consuming OpenStreetMap and friends in your JS mapping library of choice, be it OpenLayers, Leaflet or any other library that can consume XYZ tile sets.

Now that our tile storage and access has been abstracted behind a Tile Set Definition resource, it paves the way for us to easily enhance the tiling capabilities in the future with:

  • Support for additional storage backends (eg. MBTiles)
  • Support for additional tile formats (eg. Vector tiles, UTFGrid)
  • Standardizing tile access (eg. WMTS)

That's not to say such items are definitely on our current roadmap, but it will make implementing such features a more realistic and feasible possibility now that the groundwork has been laid in this release.

For more information about these new Tile Service Enhancements, you can consult MapGuide RFC 140

5 comments:

Chris said...

GREAT NEWS!!!
this feature enables several use-cases
for our applications..
also possible future MBtiles support would be so great!
any hint on when there'll be a first beta?
thx Jacky for all your hard work!
Chris.

Jackie Ng said...

Beta 1 is already out

http://themapguyde.blogspot.com.au/2015/03/announcing-mapguide-open-source-30-beta.html

Chris said...

Dos this mean if you had Raster Tile sets from something like MapTiler Mapguide3.0 could read them.

Chris Payne

Chris said...

Does this mean that Mapguide 3.0 could read Raster Tile Sets from something like Maptiler?

Chris Payne

Unknown said...

Can you provide an example of how to add WMTS service to MapGuide?