-
Notifications
You must be signed in to change notification settings - Fork 1
/
osm-tiles.js
158 lines (144 loc) · 6.18 KB
/
osm-tiles.js
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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
// AFrame component to load OpenStreetMap tiles around a given lat/lon, usually on a flat plane
//
// Internally we have to deal with 3 coordinate systems:
// * Geocoordinates (lat, lon) in degrees
// -180 180
// 90 +-----------+-----------+
// | | |
// | | |
// +-----------+-----------+
// | | |
// | | |
// -90 +-----------+-----------+
//
// * Tile coordinates (x, y), i.e. 0 to 2^zoom - 1, as the map is divided into tiles
// Tiles use the Web Mercator projection, assuming the earth is a sphere
// See https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames
// 0 2^zoom - 1
// 0 +-----------+-----------+
// | 0,0 | 1,0 | coordinates inside tiles for zoom level 1
// | | |
// +-----------+-----------+
// | 0,1 | 1,1 |
// | | |
// 2^zoom - 1 +-----------+-----------+
//
// * Plane coordinates (x, y) in meters, we take the start lat/lon as origin (0,0)
// -inf inf
// -inf +-----------+-----------+
// | | 0,-1 |
// | -2,0 0,0 2,0 |
// +-----------+-----------+
// | | |
// | | |
// inf +-----------+-----------+
AFRAME.registerComponent('osm-tiles', {
schema: {
lat: {type: 'number'},
lon: {type: 'number'},
radius_m: {type: 'number', default: 500},
zoom: {type: 'number', default: 17},
trackId: {type: 'string'}, // component's id whose position we track for dynamic tile loading
url: {type: 'string', default: 'https://tile.openstreetmap.org/'} // tileServer base url
},
init: function () {
// console.log(this.data);
this.tilesLoaded = new Set(); // contains each x,y tile id that has been added
},
// recreate the tiles layer
update: function (oldData) {
if (this.data !== oldData) {
this.trackElement = null;
this.trackPosition = null;
// reset the layer
this.el.innerHTML = '';
this.tilesLoaded.clear();
this.tileSize_m = this.lat2tileWidth_m(this.data.lat, this.data.zoom);
this.tileBase = this.latlon2fractionalTileId(this.data.lat, this.data.lon);
this.loadTilesAround(new THREE.Vector3(0, 0, 0));
// if trackId attribute is given, keep track of the element's position
if (this.data.trackId) {
let element = document.getElementById(this.data.trackId);
if (element && element.object3D) {
this.trackElement = element;
this.trackPosition = new THREE.Vector3();
}
}
}
},
tick: function () {
if (this.trackElement) {
// use world position to support movement of both head and rig
this.trackElement.object3D.getWorldPosition(this.trackPosition);
this.loadTilesAround(this.trackPosition);
}
},
// Convert latitude to width in meters for given zoom level
lat2tileWidth_m: function(lat, zoom) {
const EQUATOR_M = 40075017; // equatorial circumference in meters
let nTiles = 2 ** zoom;
let circumference_m = EQUATOR_M * Math.cos(lat * Math.PI / 180);
return circumference_m / nTiles;
},
// Convert geocoordinates to tile coordinates for given zoom level
// Returns floating point values where
// * the integer part is the tile id
// * the fractional part is the position within the tile
latlon2fractionalTileId: function(lat, lon) {
let nTiles = 2 ** this.data.zoom;
let latRad = lat * Math.PI / 180;
let x = nTiles * (lon + 180) / 360;
let y = nTiles * (1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2;
return [x, y];
},
// Create an Aframe plane with a given tile's image url, and size and position in meters
// The plane position sets x,y although Aframe uses x,z for 3D, so needs to be rotated later
createTile: function(x_m, y_m, url) {
// console.log(x_m, y_m, url, this.tileSize_m);
let tile = document.createElement('a-plane');
tile.setAttribute('src', url);
tile.setAttribute('width', this.tileSize_m);
tile.setAttribute('height', this.tileSize_m);
tile.setAttribute('position', {x: x_m, y: y_m, z: 0});
return tile;
},
// Create an OpenStreetMap tile for given x,y tile coordinates and zoom level
// Example url for Berlin center at zoom level 14: https://tile.openstreetmap.org/14/8802/5373.png
// tileSize_m sets the width and length of the tile in meters
// for real-world size this depends on the zoom level and the latitude of the origin
// tileBase is the (0,0) origin of the Aframe plane in tile coordinates [x,y]
// e.g. [8802.5, 5373.5] for the middle of the Berlin center tile at zoom level 14
loadTile: function(x, y) {
let url = this.data.url + `${this.data.zoom}/${x}/${y}.png`;
let x_m = (x - this.tileBase[0] + 0.5) * this.tileSize_m;
let y_m = (y - this.tileBase[1] + 0.5) * this.tileSize_m;
let tile = this.createTile(x_m, -y_m, url);
// let tile = this.createTile(x_m / this.tileSize_m, -y_m / this.tileSize_m, url, 1, 1);
return tile;
},
// Check if all tiles within the default radius around the given position are loaded, load if not
// pos is the position in meters on the Aframe plane, we ignore the height
loadTilesAround: function(pos) {
let tileX = this.tileBase[0] + pos.x / this.tileSize_m;
let tileY = this.tileBase[1] + pos.z / this.tileSize_m;
let radius = this.data.radius_m / this.tileSize_m;
let nTiles = 2 ** this.data.zoom;
let startX = Math.floor(tileX - radius);
let startY = Math.max(0, Math.floor(tileY - radius));
let endX = Math.ceil(tileX + radius);
let endY = Math.min(nTiles, Math.ceil(tileY + radius));
// using modulo for horizontal axis to wrap around the date line
startX = (startX + nTiles) % nTiles;
endX = (endX + nTiles) % nTiles;
for (let y = startY; y < endY; y++) {
for (let x = startX; x < endX; x++) {
let xy = (y << this.data.zoom) + x;
if (!this.tilesLoaded.has(xy)) {
let tile = this.loadTile(x, y);
this.el.appendChild(tile);
this.tilesLoaded.add(xy);
}
}
}
}
});