Skip to main content

Tutorial 5: WebApp Layer - Tile Pyramid

Introduction

This tutorial demonstrates how to create a layer using OpenStreetMap's tile pyramid that displays the appropriate map (Level of Detail) as it expands and scrolls, utilizing SVGMap's WebApp Layer mechanism.

Click here to see how it works .

File Structure

The file structure is as follows:

  • the tutorial5 directory contains the following files.
    • Tutorial5.html
      • HTML for Tutorial 5. Same content as tutorial2b.html.
    • Container.svg
      • Load dynamicOSM_r11.svg to display an external OpenStreetMap.
    • dynamicOSM_r11.svg
      • A layer containing SVGMap content that displays an external OpenStreetMap (it's empty inside, and the underlying web application dynamically constructs the DOM).
    • dynamicOSM_r11.html
      • The HTML of the web application associated with the above dynamicOSM_r11.svg
    • dynamicOSM_r11.js
      • The JavaScript code for the web application linked to the above dynamicOSM_r11.svg file.

Tutorial

SVGMap.js allows layers to be more than just content (SVG files); they can also provide a WebApp that dynamically generates SVGMap content via JavaScript. (Dynamic Layers by WebApp (WebApp Layer)) (Layer-Specific UI)

As the name WebApp Layer suggests, it can associate not only with JavaScript code but also with UI elements (Window objects) created using HTML and CSS.

From Tutorial 5 onwards, we will primarily use this WebApp Layer functionality to implement features that display various data and provide a user interface specific to each layer.

Here, we'll use this feature to build a layer that uses OpenStreetMap's tile pyramid to quickly display the appropriate map as you zoom in and out.

Note that Tutorial 5 contains a fair amount of code, so if you want to try out the WebApp Layer's features with less code, Tutorial 6 might be easier.

Click here to see how it works . The file used is a ZIP archive file.

Tutorial5.html

  • This loads the core SVGMap program file (SVGMapLv0.1_r18module.js) and makes the various SVGMap APIs available.
  • The map display area is defined (using a DIV), and an SVG file (Containers.svg) containing the layers to be displayed is loaded into it (the layers that are automatically made visible by the SVGMap core program mentioned above will be displayed).
  • Define the display and actions of the zoom up, zoom down, and GPS buttons (calling the respective APIs of the SVGMap core program).
    • Zoom-up button: Zooms in on the map by calling the svgMap.zoomup() API.
    • Zoom down button: Zooms down the map by calling the svgMap.zoomdown() API.
    • GPS button: Calls the svgMap.gps() API to zoom in and display the current location (PC or smartphone location, only if it can be identified).
  • A cross symbol is displayed to indicate the center.
  • The crosshairs above indicate the latitude and longitude displayed on the map (in reality, the latitude and longitude of the map's center are displayed when the map is moved).
<!DOCTYPE html>
<html>
<title>SVGMapLevel0.1-Rev14-Draft Tutorial5 DynamicContents</title>
<!-- Definition that the viewport display area is the entire screen -->
<meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1.0,maximum-scale=1.0" />
<meta charset="UTF-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

<!-- Loading the SVGMap core API -->
<script type="module">
import { svgMap } from 'https://cdn.jsdelivr.net/gh/svgmap/svgmapjs@latest/SVGMapLv0.1_r18module.js';
window.svgmap=svgmap
</script>

<body bgcolor="#ffffff" style="overflow:hidden;" >
<!-- Loading a container file (Container.svg) containing multiple map SVG files (only 5 files in this tutorial) -->
<div id="mapcanvas" data-src="Container.svg"></div>
<div id="gui">
<!-- Zoom-up button -->
<img id="zoomupButton" style="left: 5px; top: 5px; position: absolute;" src="./img/zoomup.png" onclick="svgMap.zoomup()" width="20" height="20" />
<!-- Zoom down button -->
<img id="zoomdownButton" style="left: 5px; top: 25px; position: absolute;" src="./img/zoomdown.png" onclick="svgMap.zoomdown()" width="20" height="20" />
<!-- GPS button -->
<img id="gpsButton" style="left: 5px; top: 45px; position: absolute;" src="./img/gps.png" onclick="svgMap.gps()" width="20" height="20" />
<!-- Title to be displayed in the upper right corner of the screen -->
<font color="blue" style="right: 5px; top: 5px; position: absolute;" >SVGMapLevel0.1 Rev14 Draft : Tutorial5 DynamicContents</font>
<!-- Display in the lower right corner of the screen -->
<font color="blue" style="right: 5px; bottom: 5px; position: absolute;" size="-2" >by SVGMap tech.</font>
<!-- Cross mark displayed in the center -->
<img id="centerSight" style="opacity:0.5" src="./img/Xcursor.png" width="15" height="15"/>
<!-- Latitude and longitude displayed as a crosshair in the lower left corner of the screen (title) -->
<font id="posCmt" size="-2" color="brown" style="left: 5px; bottom: 5px; position: absolute;">Lat,Lng:</font>
<!-- The crosshairs displayed in the lower left corner of the screen show the latitude and longitude (initial display of actual values) -->
<font id="centerPos" size="-2" color="brown" style="left: 50px; bottom: 5px; position: absolute;" >lat , lng</font>
</div>
</body>
</html>

Container.svg

  • Load the SVG files for each layer to be displayed (only dynamicOSM_r10.svg is loaded).
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:go="http://purl.org/svgmap/profile" viewBox="12300 -4600 2200 2200" >

<globalCoordinateSystem srsName="http://purl.org/crs/84" transform="matrix(100.0,0.0,0.0,-100.0,0.0,0.0)" />

<!-- Load the SVG file for OpenStreetMap as the display state -->
<animation x="-30000" y="-30000" width="60000" height="60000" xlink:href="dynamicOSM_r11.svg" title="OpenStreetMap(Global)" class="basemap switch" visibility="visible"/>
</svg>

dynamicOSM_r11.svg

An SVG map content layer file with a web application linked to display an external OpenStreetMap.

We will utilize the WebApp Layer mechanism provided by SVGMap.js .

  • By referencing the web application (HTML content containing JavaScript code) in the data-controller attribute of the SVG content's document element (svg element), the web application is linked to the SVGMap content layer.
    • The #exec=hiddenOnLayerLoad hash will launch the web application in a hidden state. See the documentation for details on layer-specific UI.
  • Because the DOM is generated directly in the web application, the content is mostly empty.
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="-42.8202042942663, -49.9999999999999, 513.842451531196, 600" data-controller="dynamicOSM_r11.html#exec=hiddenOnLayerLoad" >

<globalCoordinateSystem srsName="http://purl.org/crs/84" transform="matrix(100,0,0,-100,0,0)" />

</svg>

dynamicOSM_r11.html, dynamicOSM_r11.js

This is a webApp ( layer-specific UI ) linked to dynamicOSM_r11.svg . WebApps linked to layers allow the use of their own APIs.

OpenStreetMap (OSM) delivers a pyramidal image divided into 256x256 pixel tiles according to the map scale . (OSM Slippy map tilenames,OSM ZoomLevels)

This will be used to implement a feature (Level of Detail) that dynamically retrieves and displays tiles appropriate to the display range and zoom level from the OpenStreetMap server .

Note: The OpenStreetMap tiles used here are uniform mesh tiles on the so-called Web Mercator projection. On the other hand, the SVGMap content used in this tutorial uses the azimuthal equidistant projection (Plate Caree: a projection that directly plots longitude and latitude on the X,Y plane, commonly used in business and technical fields). Therefore, projection conversion is necessary, but for simplicity, this tutorial only uses a linear conversion of the tiles. At small scales, such as the size of the Japanese archipelago, you may see some discrepancies due to the difference in projections. (SVGMap.js also supports more accurate projection conversion.)

dynamicOSM_r11.html

The HTML consists of a common library (svgMapLayerLib.js) for initializing the layer-specific UI , a script code section describing the layer-specific processing dynamicOSM_r11.js, and the UI (HTML, CSS, etc.).

dynamicOSM_r11.html

<!doctype html> 
<html>
<head>
<meta charset="utf-8"></meta>
<title>OpenStreerMap Dynamic Layer</title>
</head>

<script src="https://cdn.jsdelivr.net/gh/svgmap/svgmapjs@latest/svgMapLayerLib.js"></script>
<script src="dynamicOSM_r11.js"></script>

<body>
<h3>OpenStreerMap Dynamic Layer</h3>
</body>
</html>

dynamicOSM_r11.js

  • onload Similar to a regular web application, this function is executed after the loading of the HTML-related resources is complete.

  • preRenderFunction() When this function is defined, it will be executed immediately before each screen redraw.

    • svgImagesProps Various information about SVG content linked to a web app.
    • svgMap.getGeoViewBox() Obtain the display area using geographic coordinates.
    • getTileSet Obtain a list of tiles to display (associative array) (details below).
    • svgImage The DOM of the linked SVGMap content
      • OSM tiles are placed in the SVG DOM as SVG image elements. Additionally, the tile number (key in the associative array) of each tile obtained with getTileSet is set in the metadata attribute.
      • When this function (preRenderFunction) is called again after scrolling, it uses the value of this metadata to ensure that tiles that have already been drawn up to the previous step and should be drawn again after scrolling remain in place.
    • getTile Based on the information (ZoomLevel, X, Y) in the tile list's associative array, obtain the image element for the tile to be retrieved.
      • OSM bit image tiles are pasted as SVG image elements.
      • getURL A function that generates the URL of an actual OSM tile from ZoomLevel, X, and Y.
      • XY2latLng A function that obtains the geographic coordinate region of a tile from ZoomLevel, X, and Y.
  • getTileSetA function that lists the tiles to be displayed at the current zoom level and display area.

    • This function retrieves the Zoom and XY coordinates of an OSM Slippy Map Tile and returns an associative array using these coordinates as keys.
    • OSM Slippy Map Tiles are tile pyramids that are recursively divided into 2x2 sections on the Web Mercator projection.
      • The Web Mercator projection constructs a square map over a range of (longitude ±180 degrees, latitude ±85.05113 degrees).
    • latLng2XY This method calculates the coordinates of a specified latitude and longitude on a bit image, assuming the entire Earth is represented as a single large square bit image on the Web Mercator projection at a specified zoom level.
      • lvl2Res Calculate the width of the corresponding bit image of the entire Earth from the ZoomLevel (Level0: 256px)
    • XY2TileXY From the XY coordinates on a large square bit image of the Earth shown above, when it is tiled, the tile number (tile's X number, Y number) to which those coordinates belong can be obtained.

    dynamicOSM_r11.js

Click see full JS data
// Dynamic OpemStreetMap Layer for SVGMap Sample for SVGMapLevel0 > r10
//
// Programmed by Satoru Takagi
//
// License: (MPL v2)
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// Prototype of a dynamic intended layer for use as an iframe
// (JavaScript can be imported and placed within the SVG content.)
// OpenStreetMap is used as the map data (this can be easily replaced with other sources).
//
//
// The following are pre-configured in the environment in which this code operates.
// document:This document itself
// svgImage: The SVGMap content associated with this document
// svgMap.getGeoViewBox(): geographical viewbox
// svgImageProps: Various properties of the SVGMap content associated with this document
// svgImageProps.scale: Scale (the scale of the coordinates of this SVG content relative to the screen coordinates)
//
// 2013/01/24 : 1st ver.
// 2022/01/31: Ported to WebApp layer

// Execute when this file is loaded.
onload = function () {
// Immediately after this script is loaded, call refreshScreen() to:
//The following preRenderFunction is executed the first time.
svgMap.refreshScreen();
};

function preRenderFunction() {
// Callback function executed immediately before redrawing
var level = 8;
// Calculate zoom level (3 to 18)
var level = Math.floor(Math.LOG2E * Math.log(svgImageProps.scale) + 7.5);
if (level > 18) {
level = 18;
} else if (level < 3) {
level = 3;
}

// Get the XY coordinates and HashKey of the tile to be displayed in the viewBox in the geographic coordinates of this map.
var tileSet = getTileSet(svgMap.getGeoViewBox(), level);

// Get the currently loaded element with the tag name "image" (the image for each map tile).
console.log("tileSet:", tileSet);
var currentTiles = svgImage.getElementsByTagName("image");

// Repeat the following for each tile obtained, reusing those already loaded and deleting those outside the display range.
for (var i = currentTiles.length - 1; i >= 0; i--) {
var oneTile = currentTiles[i];
var qkey = oneTile.getAttribute("metadata");
if (tileSet[qkey]) {
// Set a flag to skip as it already exists.
tileSet[qkey].exist = true;
else {
// It doesn't exist, so delete it.
oneTile.parentNode.removeChild(oneTile);
}
}

// Repeat the following steps for the number of tiles to be displayed, adding any unloaded files to the load elements.
for (var tkey in tileSet) {
if (!tileSet[tkey].exist) {
var addTile = getTile(tileSet[tkey].x, tileSet[tkey].y, level, this.CRS);
svgImage.getElementsByTagName("svg")[0].appendChild(addTile);
}
}
}

// Get tiles (divided map image) for the specified location
function getTile(tileX, tileY, level, crs) {
// Get the URL of the tile with coordinates tileX, tileY, and zoom level.
var tileURL = getURL(tileX, tileY, level);

// Get the bbox in the SVG of the tile
var tLatLng = XY2latLng(tileX * tilePix, tileY * tilePix, level);
var tSvg = svgMap.transform(tLatLng.lng, tLatLng.lat, crs);
var tLatLngBR = XY2latLng(
tileX * tilePix + tilePix,
tileY * tilePix + tilePix,
level
);
var tSvgBR = svgMap.transform(tLatLngBR.lng, tLatLngBR.lat, crs);
tSvg.width = tSvgBR.x - tSvg.x; // Inefficient... Improvement to be postponed
tSvg.height = tSvgBR.y - tSvg.y;

// Create the tile elements to be retrieved and set their respective attributes.
var cl = svgImage.createElement("image");
cl.setAttribute("x", tSvg.x);
cl.setAttribute("y", tSvg.y);
cl.setAttribute("width", tSvg.width);
cl.setAttribute("height", tSvg.height);
cl.setAttribute("xlink:href", tileURL.URL);
cl.setAttribute("metadata", tileURL.Key);

return cl;
}

// Returns the required XY set of tiles when displaying a map at the specified map coordinates in a geoViewBox with a zoom level of level.
function getTileSet(geoViewBox, level) {
var TileSet = new Object();
if (geoViewBox.y + geoViewBox.height > 85.05113) {
geoViewBox.height = 85.05113 - geoViewBox.y;
}

if (geoViewBox.y < -85.05113) {
geoViewBox.y = -85.05113;
}

// Returns the XY coordinates of a tile in the specified area and its HashKey.
var tlxy = latLng2XY(geoViewBox.y + geoViewBox.height, geoViewBox.x, level);
var tileTLxy = XY2TileXY(tlxy);
var brxy = latLng2XY(geoViewBox.y, geoViewBox.x + geoViewBox.width, level);
var tileBRxy = XY2TileXY(brxy);

// Repeat the following for the number of tiles required for the desired height and width.
for (var i = tileTLxy.y; i <= tileBRxy.y; i++) {
for (var j = tileTLxy.x; j <= tileBRxy.x; j++) {
// Get the HashKey from the tile's XY coordinates and zoom level
var qkey = getKey(j, i, level);
// Set the necessary tile information for each HashKey obtained above.
TileSet[qkey] = new Object();
TileSet[qkey].x = j;
TileSet[qkey].y = i;
}
}
return TileSet;
}

// Convert latitude and longitude to X and Y coordinates
function latLng2XY(lat, lng, lvl) {
var size = lvl2Res(lvl);
var sinLat = Math.sin((lat * Math.PI) / 180.0);
var pixelX = ((lng + 180.0) / 360.0) * size;
var pixelY =
(0.5 - Math.log((1 + sinLat) / (1.0 - sinLat)) / (4 * Math.PI)) * size;
return {
x: pixelX,
y: pixelY,
};
}

// Convert from XY to tile XY
function XY2TileXY(xy) {
var tileX = Math.floor(xy.x / tilePix);
var tileY = Math.floor(xy.y / tilePix);
return {
x: tileX,
y: tileY,
};
}

var tilePix = 256;
// Returns the size of a tile piece from the zoom level.
function lvl2Res(lvl) {
var j = 1;
for (var i = 0; i < lvl; i++) {
j = j * 2;
}
return j * tilePix;
}

// Convert from XY to latitude and longitude
function XY2latLng(px, py, lvl) {
var size = lvl2Res(lvl);
var x = px / size - 0.5;
var y = 0.5 - py / size;
var lat = 90 - (360 * Math.atan(Math.exp(-y * 2 * Math.PI))) / Math.PI;
var lng = 360 * x;
return {
lat: lat,
lng: lng,
};
}

var sva = new Array("a", "b", "c");
var svNumb = 0;
var culture = "en-US";
var bingRoadSearchPart = ".jpeg?g=849&mkt=" + culture + "&shading=hill";

// Returns a URL based on the tile's XY coordinates and zoom level.
function getURL(tx, ty, lvl) {
// Get a HashKey from XY and zoom level
var tile_ans = getKey(tx, ty, lvl);
// Assemble the OpenStreetMap URL
var mapServerURL =
"http://" +
sva[svNumb] +
".tile.openstreetmap.org/" +
lvl +
"/" +
tx +
"/" +
ty +
".png";
// Load balancing is performed when acquiring map images by sequentially switching between multiple similar servers.
++svNumb;
if (svNumb > 2) {
svNumb = 0;
}
return {
URL: mapServerURL,
Key: tile_ans,
};
}

// Generate and return a HashKey
function getKey(tx, ty, lvl) {
return tx + "_" + ty + "_" + lvl;
}