map-tile-downloader
Version:
This Package developed for downloading map raster tiles
468 lines (447 loc) • 12.9 kB
JavaScript
const TilesCounter = require('map-tiles-generator');
const bboxPol = require('@turf/bbox-polygon');
const wkt = require('wkt');
const fs = require('fs');
const JSZip = require('jszip');
const axios = require('axios');
class MapTileDownloader {
constructor(obj) {
try {
var requiredProps = ['tile', 'area'];
var data = this.getFromOBJ(obj, requiredProps);
this.tile = this.checkTile(data);
this.area = this.checkArea(data);
this.status = true;
} catch (error) {
console.error('Error :', error.message);
}
}
checkArea(data) {
var requiredAreaProps = ['type', 'data'];
var requiredAreaTypeProps = ['bbox', 'geojson', 'wkt'];
var area = this.getFromOBJ(data.area, requiredAreaProps);
var areaCheck1 = this.objectValidate(area, {
type: {
type: 'string',
controls: [{
type: 'in',
data: requiredAreaTypeProps
}]
},
data: {
type: 'any',
controls: []
}
});
if (area.type == 'bbox') {
var areaCheck = this.objectValidate(areaCheck1, {
type: {
type: 'string',
controls: [{
type: 'in',
data: requiredAreaTypeProps
}]
},
data: {
type: 'array',
controls: [{
type: 'size',
data: 4
}]
}
});
areaCheck.data = bboxPol.default(areaCheck.data);
return areaCheck;
} else if (area.type == 'geojson') {
var areaCheck = this.objectValidate(areaCheck1, {
type: {
type: 'string',
controls: [{
type: 'in',
data: requiredAreaTypeProps
}]
},
data: {
type: 'object',
controls: [{
type: 'polygon',
data: null
}]
}
});
return areaCheck;
} else if (area.type == 'wkt') {
var areaCheck = this.objectValidate(areaCheck1, {
type: {
type: 'string',
controls: [{
type: 'in',
data: requiredAreaTypeProps
}]
},
data: {
type: 'string',
controls: [{
type: 'contains',
data: ['POLYGON', '(', ',', ')']
}]
}
});
areaCheck.data = {
type: 'Feature',
properties: {},
geometry: wkt.parse(areaCheck.data)
};
return areaCheck;
} else {
throw new Error(`Area options property is not walid!`);
}
}
checkTile(data) {
var requiredTileProps = ['type', 'url', 'subdomains', 'minZoom', 'maxZoom', 'format'];
var tile = this.getFromOBJ(data.tile, requiredTileProps);
var tileCheck = this.objectValidate(tile, {
type: {
type: 'string',
controls: [{
type: 'in',
data: ['url', 'wms', 'wfs']
}]
},
url: {
type: 'string',
controls: [{
type: 'contains',
data: ['{x}', '{y}', '{z}', 'http']
}]
},
subdomains: {
type: 'array',
controls: []
},
minZoom: {
type: 'integer',
controls: [{
type: 'range',
min: 0,
max: 22
}]
},
maxZoom: {
type: 'integer',
controls: [{
type: 'range',
min: 0,
max: 22
}]
},
format: {
type: 'string',
controls: [{
type: 'in',
data: ['png', 'gif', 'pbf', 'jpg', 'jpeg']
}]
},
});
return tileCheck;
}
getFromOBJ(obj, props) {
var ret = {};
for (var i = 0; i < props.length; i++) {
if (obj.hasOwnProperty(props[i])) {
ret[props[i]] = obj[props[i]];
} else {
throw new Error(`Options object must have ${props.join(', ')} prop name`);
}
}
return ret;
}
typeValidate(val, type) {
switch (type) {
case 'any': {
return true;
}
case 'string': {
return typeof val === 'string';
}
case 'integer': {
return typeof val === 'number' && Number.isInteger(val);
}
case 'float': {
return typeof val === 'number' && Number.isFinite(val);
}
case 'object': {
return typeof val === 'object';
}
case 'array': {
return Array.isArray(val)
}
}
}
dataControl(val, control, prop) {
if (control.length == 0) {
return true;
}
switch (control.type) {
case 'range': {
var min = control.min;
var max = control.max;
return val >= min && val <= max;
}
case 'polygon': {
if (val['type'] !== 'Feature') {
throw new Error(`GeoJSON must have type(Feature) property`);
}
if (val['geometry'] == undefined) {
throw new Error(`GeoJSON must have geometry property`);
}
if (val.geometry['type'] !== 'Polygon') {
throw new Error(`GeoJSON geometry must be Polygon`);
}
return true;
}
case 'in': {
var inlist = control.data;
return inlist.indexOf(val) !== -1
}
case 'contains': {
var inlist = control.data;
var notFound = [];
for (var i = 0; i < inlist.length; i++) {
if (val.indexOf(inlist[i]) === -1) {
notFound.push(inlist[i]);
}
}
if (notFound.length > 0) {
throw new Error(`Prop ${notFound.join(', ')} Not Found`);
} else {
return true;
}
}
case 'size': {
var len = control.data;
if (val.length === len) {
return true;
} else {
throw new Error(`Prop ${prop} must have ${len} Items`);
}
}
}
return false;
}
objectValidate(obj, rules) {
var ret = {};
for (var prop in rules) {
var data = obj[prop];
var checkType = this.typeValidate(data, rules[prop].type);
if (checkType == false) {
throw new Error(`Data type is not valid ${prop}`);
}
for (var i = 0; i < rules[prop].controls.length; i++) {
var ctrl = rules[prop].controls[i];
var controls = this.dataControl(data, ctrl, prop);
if (controls == false) {
throw new Error(`${prop} property data did not pass in ${ctrl.type} control`);
}
}
ret[prop] = data;
}
return ret;
}
random(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
generateTile(url, subdomains, x, y, z) {
var rnd = this.random(0, subdomains.length) % subdomains.length;
var sname = subdomains[rnd];
if (url.indexOf('{s}') !== -1) {
while (url.indexOf('{s}') != -1) {
url = url.replace('{s}', sname);
}
}
if (url.indexOf('{z}') !== -1) {
while (url.indexOf('{z}') != -1) {
url = url.replace('{z}', z);
}
}
if (url.indexOf('{y}') !== -1) {
while (url.indexOf('{y}') != -1) {
url = url.replace('{y}', y);
}
}
if (url.indexOf('{x}') !== -1) {
while (url.indexOf('{x}') != -1) {
url = url.replace('{x}', x);
}
}
return url;
}
downloadList(zip, i, arr, type, path, callback, progressCallback) {
if (i < arr.length) {
var item = arr[i];
axios({
method: 'get',
url: item.url,
responseType: 'stream'
})
.then(response => {
if (type == 'zip') {
zip.file(`zoom_levels/${item.z}/${item.x}/${item.y}`, response.data);
i++;
if (progressCallback) {
progressCallback({
progress: (i / arr.length) * 100,
current: i,
total: arr.length,
tile:{z: item.z, x: item.x, y: item.y},
status:'success'
});
}
this.downloadList(zip, i, arr, type, path, callback, progressCallback);
} else if (type == 'folder') {
const chunks = [];
response.data.on('data', chunk => chunks.push(chunk));
response.data.on('end', () => {
const fileBuffer = Buffer.concat(chunks);
fs.writeFileSync(`${path}/${item.z}/${item.x}/${item.y}`, fileBuffer);
i++;
if (progressCallback) {
progressCallback({
progress: (i / arr.length) * 100,
current: i,
total: arr.length,
tile:{z: item.z, x: item.x, y: item.y},
status:'success'
});
}
this.downloadList(zip, i, arr, type, path, callback, progressCallback);
});
response.data.on('error', error => {
i++;
if (progressCallback) {
progressCallback({
progress: (i / arr.length) * 100,
current: i,
total: arr.length,
tile:{z: item.z, x: item.x, y: item.y},
status:'faild'
});
}
this.downloadList(zip, i, arr, type, path, callback, progressCallback);
throw new Error(`Download Error!`);
});
}
})
.catch(error => {
i++;
if (progressCallback) {
progressCallback({
progress: (i / arr.length) * 100,
current: i,
total: arr.length,
tile:{z: item.z, x: item.x, y: item.y},
status:'error'
});
}
this.downloadList(zip, i, arr, type, path, callback, progressCallback);
throw new Error(`Download Error!`);
});
} else {
if (progressCallback) {
progressCallback({
progress: 100,
current: arr.length,
total: arr.length,
tile:{z: 0, x: 0, y: 0},
status:'completed'
});
}
if (type == 'zip') {
zip.generateAsync({
type: "nodebuffer"
}).then((content) => {
callback(content);
});
} else if (type == 'folder') {
callback(`${path}`);
}
}
}
downloadZipToPath(path, callback) {
this.getAsZip((zipFile) => {
fs.writeFile(path, zipFile, (err) => {
if (err) {
callback(false)
} else {
callback(true)
}
});
})
}
getAsZip(callback) {
if (this.status) {
var format = this.tile.format;
const zip = new JSZip();
zip.folder('zoom_levels');
const generator = new TilesCounter(this.area.data);
var tiles = generator.getTilesFromZoomRange(this.tile.minZoom, this.tile.maxZoom);
var downloadlist = [];
for (var level in tiles.zoom) {
zip.folder(`zoom_levels/${level}`);
for (var i = 0; i < tiles.zoom[level].length; i++) {
var x = tiles.zoom[level][i].x;
var y = tiles.zoom[level][i].y;
zip.folder(`zoom_levels/${level}/${x}`);
var z = tiles.zoom[level][i].z;
var tile = this.generateTile(this.tile.url, this.tile.subdomains, x, y, z);
var yname = `${y}.${format}`;
downloadlist.push({
url: tile,
z: z,
x: x,
y: yname
});
}
}
this.downloadList(zip, 0, downloadlist, 'zip', '', callback);
} else {
throw new Error(`Object status is not active!`);
}
}
generateToPath(path, callback, progressCallback) {
if (this.status) {
var format = this.tile.format;
if (!fs.existsSync(`${path}`)) {
fs.mkdirSync(`${path}`);
}
const generator = new TilesCounter(this.area.data);
var tiles = generator.getTilesFromZoomRange(this.tile.minZoom, this.tile.maxZoom);
var downloadlist = [];
for (var level in tiles.zoom) {
if (!fs.existsSync(`${path}/${level}`)) {
fs.mkdirSync(`${path}/${level}`);
}
for (var i = 0; i < tiles.zoom[level].length; i++) {
var x = tiles.zoom[level][i].x;
var y = tiles.zoom[level][i].y;
if (!fs.existsSync(`${path}/${level}/${x}`)) {
fs.mkdirSync(`${path}/${level}/${x}`);
}
var z = tiles.zoom[level][i].z;
var tile = this.generateTile(this.tile.url, this.tile.subdomains, x, y, z);
var yname = `${y}.${format}`;
downloadlist.push({
url: tile,
z: z,
x: x,
y: yname
});
}
}
this.downloadList({}, 0, downloadlist, 'folder', path, callback, progressCallback);
} else {
throw new Error(`OBject status is not active!`);
}
}
}
module.exports = MapTileDownloader;