mirror of
https://github.com/protomaps/PMTiles.git
synced 2026-02-04 19:01:08 +00:00
add leaflet example, js implementation
This commit is contained in:
30
examples/leaflet.html
Normal file
30
examples/leaflet.html
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
</head>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css"/>
|
||||||
|
<script src="https://unpkg.com/leaflet@1.6.0/dist/leaflet.js"></script>
|
||||||
|
<script src="../js/pmtiles.js"></script>
|
||||||
|
<style>
|
||||||
|
body, #map {
|
||||||
|
height:100vh;
|
||||||
|
margin:0px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="map"></div>
|
||||||
|
<script>
|
||||||
|
const map = L.map('map')
|
||||||
|
const ready = metadata => {
|
||||||
|
map.options.maxZoom = +metadata.maxzoom
|
||||||
|
const b = metadata.bounds.split(',')
|
||||||
|
map.fitBounds([[+b[1],+b[0]],[+b[3],+b[2]]],{maxZoom:+metadata.maxzoom})
|
||||||
|
}
|
||||||
|
const p = new pmtiles.PMTiles('http://localhost:8500/example.pmtiles',ready);
|
||||||
|
p.leafletLayer().addTo(map)
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
137
js/pmtiles.js
137
js/pmtiles.js
@@ -1,11 +1,140 @@
|
|||||||
(function (root, factory) {
|
(function (root, factory) {
|
||||||
if (typeof define === 'function' && define.amd) {
|
if (typeof define === 'function' && define.amd) {
|
||||||
define([], factory);
|
define([], factory)
|
||||||
} else if (typeof module === 'object' && module.exports) {
|
} else if (typeof module === 'object' && module.exports) {
|
||||||
module.exports = factory();
|
module.exports = factory()
|
||||||
} else {
|
} else {
|
||||||
root.pmtiles = factory();
|
root.pmtiles = factory()
|
||||||
}
|
}
|
||||||
}(typeof self !== 'undefined' ? self : this, function () {
|
}(typeof self !== 'undefined' ? self : this, function () {
|
||||||
return {pmtiles:true};
|
const getUint24 = (dataview, pos) => {
|
||||||
|
return (dataview.getUint16(pos+1,true) << 8) + dataview.getUint8(pos,true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUint48 = (dataview, pos) => {
|
||||||
|
return (dataview.getUint32(pos+2,true) << 16) + dataview.getUint16(pos,true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseHeader = dataview => {
|
||||||
|
var magic = dataview.getUint16(0,true)
|
||||||
|
// assert that the magic number (2 bytes) matches 19792
|
||||||
|
var version = dataview.getUint16(2,true)
|
||||||
|
var json_size = dataview.getUint32(4,true)
|
||||||
|
var root_entries = dataview.getUint16(8,true)
|
||||||
|
return {version:version,json_size:json_size,root_entries:root_entries}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bytesToMap = dataview => {
|
||||||
|
let m = new Map()
|
||||||
|
for (var i = 0; i < dataview.byteLength; i+=17) {
|
||||||
|
var z_raw = dataview.getUint8(i,true)
|
||||||
|
var z = z_raw & 127
|
||||||
|
var is_dir = z_raw >> 7
|
||||||
|
var x = getUint24(dataview,i+1)
|
||||||
|
var y = getUint24(dataview,i+4)
|
||||||
|
var offset = getUint48(dataview,i+7)
|
||||||
|
var length = dataview.getUint32(i+13,true)
|
||||||
|
m.set(z + '_' + x + '_' + y,[offset,length,is_dir])
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
class PMTiles {
|
||||||
|
constructor(url,ready) {
|
||||||
|
this.url = url
|
||||||
|
this.apex = fetch(this.url,{method:'HEAD',headers:{'Range':'bytes=0-511999'}}).then(resp => {
|
||||||
|
if (resp.status == 206) { // this does not work on Azure, it returns 200 instead of 206
|
||||||
|
console.log("Check succeeded: server supports byte ranges")
|
||||||
|
return fetch(this.url,{headers:{'Range':'bytes=0-511999'}}).then(resp => {
|
||||||
|
return resp.arrayBuffer()
|
||||||
|
}).then(buf => {
|
||||||
|
const header = parseHeader(new DataView(buf,0,10))
|
||||||
|
var dec = new TextDecoder("utf-8")
|
||||||
|
if (ready) {
|
||||||
|
ready(JSON.parse(dec.decode(new DataView(buf,10,header.json_size))))
|
||||||
|
}
|
||||||
|
return bytesToMap(new DataView(buf,10+header.json_size,17*header.root_entries))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// TODO make this LRU
|
||||||
|
this.leaves = new Map()
|
||||||
|
this.outstanding_requests = new Map()
|
||||||
|
}
|
||||||
|
|
||||||
|
getLeaf = tilestr => {
|
||||||
|
return new Promise((resolve,reject) => {
|
||||||
|
if (this.leaves.has(tilestr)) {
|
||||||
|
resolve(this.leaves.get(tilestr))
|
||||||
|
} else if (this.outstanding_requests.has(tilestr)) {
|
||||||
|
this.outstanding_requests.get(tilestr).push(resolve)
|
||||||
|
} else {
|
||||||
|
this.outstanding_requests.set(tilestr,[])
|
||||||
|
this.apex.then(apex_map => {
|
||||||
|
if (apex_map.has(tilestr)) {
|
||||||
|
var val = apex_map.get(tilestr)
|
||||||
|
if (val[2] == 1) { // it is a directory
|
||||||
|
fetch(this.url, {headers:{'range':'bytes=' + val[0] + '-' + (val[0] + val[1]-1)}}).then(resp => {
|
||||||
|
return resp.arraybuffer()
|
||||||
|
}).then(buf => {
|
||||||
|
var map = bytestomap(buf,val[1]/17)
|
||||||
|
this.leaves.set(tilestr,map)
|
||||||
|
resolve(map)
|
||||||
|
this.outstanding_requests.get(tilestr).foreach(f => {
|
||||||
|
f(map)
|
||||||
|
})
|
||||||
|
console.log("leaves: ", this.leaves.size)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
leafletLayer = options => {
|
||||||
|
const self = this
|
||||||
|
var cls = L.GridLayer.extend({
|
||||||
|
createTile: function(coords, done){
|
||||||
|
var tile = document.createElement('img');
|
||||||
|
var error
|
||||||
|
|
||||||
|
self.apex.then(map => {
|
||||||
|
var strid = coords.z + '_' + coords.x + '_' + coords.y
|
||||||
|
if (map.has(strid)) {
|
||||||
|
var val = map.get(strid)
|
||||||
|
fetch(self.url,{headers:{'Range':'bytes=' + val[0] + '-' + (val[0]+val[1]-1)}}).then(resp => {
|
||||||
|
return resp.arrayBuffer()
|
||||||
|
}).then(buf => {
|
||||||
|
var blob = new Blob( [buf], { type: "image/png" } );
|
||||||
|
var imageUrl = window.URL.createObjectURL(blob);
|
||||||
|
tile.src = imageUrl;
|
||||||
|
done(error,tile)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return tile;
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
_removeTile: function (key) {
|
||||||
|
var tile = this._tiles[key]
|
||||||
|
if (!tile) { return }
|
||||||
|
tile.el.width = 0
|
||||||
|
tile.el.height = 0
|
||||||
|
tile.el.deleted = true
|
||||||
|
L.DomUtil.remove(tile.el)
|
||||||
|
delete this._tiles[key]
|
||||||
|
this.fire('tileunload', {
|
||||||
|
tile: tile.el,
|
||||||
|
coords: this._keyToTileCoords(key)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return new cls()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {PMTiles:PMTiles}
|
||||||
}));
|
}));
|
||||||
Reference in New Issue
Block a user