agsout
Version:
Converts ArcGIS Server Map Service to Shapefile
247 lines (218 loc) • 6.77 kB
JavaScript
// @Author: Joshua Tanner
// @Date: 1/15/2018
// @Description: Easy way to convert ArcGIS Server service to GeoJSON
// and shapefile format. Good for backup solution.
// @services.txt format :: serviceLayerURL|layerName|throttle(ms)
// @githubURL : https://github.com/tannerjt/AGStoShapefile
// Node Modules
const fs = require('fs');
const rp = require('request-promise');
const request = require('request');
const _ = require('lodash');
const TerraformerArcGIS = require('terraformer-arcgis-parser');
const geojsonStream = require('geojson-stream');
const JSONStream = require('JSONStream');
const CombinedStream = require('combined-stream');
const queryString = require('query-string');
const merge2 = require('merge2');
const rimraf = require('rimraf');
const ogr2ogr = require('ogr2ogr');
// ./mixin.js
// merge user query params with default
const mixin = require('./mixin');
var program = require('commander');
program
.version('1.0.2')
.option('-o, --outdir [directory]', 'Output directory')
.option('-s, --services [path to txt file]', 'Text file containing service list to extract')
.option('-S, --shapefile', 'Optional export to shapefile')
.parse(process.argv);
const serviceFile = program.services || 'services.txt';
var outDir = program.outdir || './output/';
// Remove trailing '/'
outDir = outDir.replace(/\/$/, '');
fs.readFile(serviceFile, function (err, data) {
if (err) {
throw err;
}
data.toString().split('\n').forEach(function (service) {
var service = service.split('|');
if(service[0].split('').length == 0) return;
var baseUrl = getBaseUrl(service[0].trim());
var reqQS = {
where: '1=1',
returnIdsOnly: true,
f: 'json'
};
var userQS = getUrlVars(service[0].trim());
// mix one obj with another
var qs = mixin(userQS, reqQS);
qs = queryString.stringify(qs);
var url = decodeURIComponent(getBaseUrl(baseUrl) + '/query/?' + qs);
var throttle = 0;
if(service.length > 2) {
throttle = +service[2];
}
rp({
url : url,
method : 'GET',
json : true
}).then((body) => {
requestService(service[0].trim(), service[1].trim(), body.objectIds, throttle);
}).catch((err) => {
console.log(err);
});
})
});
// Resquest JSON from AGS
function requestService(serviceUrl, serviceName, objectIds, throttle) {
objectIds.sort();
const requests = Math.ceil(objectIds.length / 100);
var completedRequests = 0;
console.log(`Number of features for service ${serviceName}:`, objectIds.length);
console.log(`Getting chunks of 100 features, will make ${requests} total requests`);
for(let i = 0; i < Math.ceil(objectIds.length / 100); i++) {
var ids = [];
if ( ((i + 1) * 100) < objectIds.length ) {
ids = objectIds.slice(i * 100, (i * 100) + 100);
} else {
ids = objectIds.slice(i * 100, objectIds[objectIds.length]);
}
// we need these query params
const reqQS = {
objectIds : ids.join(','),
geometryType : 'esriGeometryEnvelope',
returnGeometry : true,
returnIdsOnly : false,
outFields : '*',
outSR : '4326',
f : 'json'
};
// user provided query params
const userQS = getUrlVars(serviceUrl);
// mix one obj with another
var qs = mixin(userQS, reqQS);
qs = queryString.stringify(qs);
const url = decodeURIComponent(getBaseUrl(serviceUrl) + '/query/?' + qs);
const partialsDir = `${outDir}/${serviceName}/partials`;
if(i == 0) {
// first pass, setup folders
if(!fs.existsSync(`${outDir}`)) {
fs.mkdirSync(`${outDir}`)
}
if(!fs.existsSync(`${outDir}/${serviceName}`)) {
fs.mkdirSync(`${outDir}/${serviceName}`);
}
if (!fs.existsSync(partialsDir)){
fs.mkdirSync(partialsDir);
} else {
rimraf.sync(partialsDir);
fs.mkdirSync(partialsDir);
}
}
const featureStream = JSONStream.parse('features.*', convert);
const outFile = fs.createWriteStream(`${partialsDir}/${i}.json`);
const options = {
url: url,
method: 'GET',
json: true,
};
// timeout for throttle
setTimeout(() => {
request(options)
.pipe(featureStream)
.pipe(geojsonStream.stringify())
.pipe(outFile)
.on('finish', () => {
completedRequests += 1;
console.log(`Completed ${completedRequests} of ${requests} requests for ${serviceName}`);
if(requests == completedRequests) {
mergeFiles();
}
})
.on('error', (err) => {
console.log(err);
});
}, i * throttle);
function convert (feature) {
if(!feature.geometry) {
console.log("Feature Missing Geometry and is Omitted: ", JSON.stringify(feature))
return null;
}
const gj = {
type: 'Feature',
properties: feature.attributes,
geometry: TerraformerArcGIS.parse(feature.geometry)
}
return gj
}
function mergeFiles() {
console.log(`Finished extracting chunks for ${serviceName}, merging files...`)
fs.readdir(partialsDir, (err, files) => {
const finalFilePath = `${outDir}/${serviceName}/${serviceName}_${Date.now()}.geojson`
const finalFile = fs.createWriteStream(finalFilePath);
let streams = CombinedStream.create();
_.each(files, (file) => {
streams.append((next) => {
next(
fs.createReadStream(`${partialsDir}/${file}`)
.pipe(JSONStream.parse('features.*'))
.on('error', (err) => {
console.log(err);
})
);
})
});
streams
.pipe(geojsonStream.stringify())
.pipe(finalFile)
.on('finish', () => {
rimraf(partialsDir, () => {
console.log(`${serviceName} is complete`);
console.log(`File Location: ${finalFilePath}`);
if(program.shapefile) {
makeShape(finalFilePath);
}
});
})
.on('error', (err) => {
console.log(err);
})
});
}
function makeShape(geojsonPath) {
console.log(`Generating shapefile for ${serviceName}`)
// todo: make optional with flag
const shpPath = `${outDir}/${serviceName}/${serviceName}_${Date.now()}.zip`;
const shpFile = fs.createWriteStream(shpPath);
var shapefile = ogr2ogr(geojsonPath)
.format('ESRI Shapefile')
.options(['-nln', serviceName])
.timeout(120000)
.skipfailures()
.stream();
shapefile.pipe(shpFile);
}
};
}
//http://stackoverflow.com/questions/4656843/jquery-get-querystring-from-url
function getUrlVars(url) {
var vars = {}, hash;
var hashes = url.slice(url.indexOf('?') + 1).split('&');
for(var i = 0; i < hashes.length; i++)
{
hash = hashes[i].split('=');
vars[hash[0].toString()] = hash[1];
}
return vars;
}
// get base url for query
function getBaseUrl(url) {
// remove any query params
var url = url.split("?")[0];
if((/\/$/ig).test(url)) {
url = url.substring(0, url.length - 1);
}
return url;
}