├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── bin └── kml-split.js ├── index.js ├── package.json └── test ├── fixtures ├── invalid.kml ├── no-layers.kml └── valid-22-layers.kml └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | test/output 4 | npm-debug.log -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - 4 -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | ISC License 3 | 4 | Copyright (c) 2017, Mapbox 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kml-split 2 | 3 | Split KML files with many layers into separate files. Go from `1 KML file with 20 layers` to `4 KML files with 5 layers`. Uses Node GDAL to read KML sources and create KML sinks. 4 | 5 | [![Build Status](https://travis-ci.com/mapbox/kml-split.svg?branch=master)](https://travis-ci.com/mapbox/kml-split) 6 | 7 | # Usage 8 | 9 | ### Install 10 | 11 | ``` 12 | # package.json 13 | npm install @mapbox/kml-split --save 14 | 15 | # globally 16 | npm install @mapbox/kml-split -g 17 | ``` 18 | 19 | ### `kmlSplit(file, options, callback)` 20 | 21 | Example 22 | 23 | ```javascript 24 | var kmlSplit = require('@mapbox/kml-split'); 25 | 26 | kmlSplit('./path/to/file.kml', {maxLayers: 5}, function(err, res) { 27 | if (err) throw err; 28 | console.log(res); // => array of file paths for newly created files 29 | }); 30 | ``` 31 | 32 | Options 33 | 34 | * `maxLayers`: the maximum number of layers per file (default: `15`) 35 | * `outDir`: output directory for the newly created files (defaults to the current directory) 36 | 37 | ### CLI 38 | 39 | ``` 40 | kml-split -l/--layers -o/--output 41 | ``` 42 | 43 | Example 44 | 45 | ``` 46 | kml-split my-awesome.kml -l 15 -o ../../output/files 47 | ``` 48 | 49 | # Test 50 | 51 | ``` 52 | npm test 53 | ``` 54 | -------------------------------------------------------------------------------- /bin/kml-split.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var argv = require('yargs').argv; 4 | var fs = require('fs'); 5 | var kmlSplit = require('..'); 6 | 7 | console.log(argv); 8 | 9 | var inFile = argv._[0]; 10 | if (!inFile) { 11 | console.log('No input file received.'); 12 | process.exit(1); 13 | } 14 | 15 | try { 16 | fs.statSync(inFile); 17 | } catch(err) { 18 | console.log('Cannot read the input file. Please make sure your path is correct.'); 19 | process.exit(1); 20 | } 21 | 22 | var options = {}; 23 | if (argv.l || argv.layers) options.maxLayers = argv.l || argv.layers; 24 | if (argv.o || argv.output) options.outDir = argv.o || argv.output; 25 | 26 | kmlSplit(inFile, options, function(err, res) { 27 | if (err) throw err; 28 | console.log('Successfully split KML files!\n'); 29 | res.forEach(function(f) { 30 | console.log(f); 31 | }); 32 | process.exit(0); 33 | }); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gdal = require('gdal'); 4 | var path = require('path'); 5 | 6 | function kmlSplit(file, options, callback) { 7 | 8 | if (!callback && typeof options === 'function') { 9 | var callback = options; 10 | } 11 | 12 | var paths = file.split('/'); 13 | var sourceName = path.basename(file, '.kml'); 14 | var outDir = options.outDir || file.replace(paths[paths.length-1], ''); 15 | var maxLayers = options.maxLayers || 15; 16 | 17 | var datasource; 18 | var wgs84; 19 | try { 20 | wgs84 = gdal.SpatialReference.fromEPSG(4326); 21 | datasource = gdal.open(file); 22 | } catch (err) { 23 | return callback(err); 24 | } 25 | 26 | var layerCount = datasource.layers.count(); 27 | if (layerCount < 1) { 28 | datasource.close(); 29 | return callback(new Error('KML does not contain any layers.')); 30 | } 31 | if (layerCount <= maxLayers) { 32 | datasource.close(); 33 | return callback(new Error('Maximum layers is larger than layer count. Nothing to do.')); 34 | } 35 | 36 | var files = []; 37 | var sinkCount = 1; 38 | var datasink = createSink(outDir, sourceName, sinkCount); 39 | files.push(sinkName(outDir, sourceName, sinkCount)); 40 | var count = 1; 41 | datasource.layers.forEach(function(layer) { 42 | var name = layer.name; 43 | 44 | // write the layer to the current datasink 45 | var newLayer = datasink.layers.create(name, wgs84, layer.geomType); 46 | layer.features.forEach(function(feature) { 47 | var geom = feature.getGeometry(); 48 | if (!geom) return; 49 | else { 50 | if (geom.isEmpty()) return; 51 | if (!geom.isValid()) return; 52 | 53 | newLayer.features.add(feature); 54 | } 55 | }); 56 | newLayer.flush(); 57 | 58 | if (count !== 1 && count % maxLayers === 0) { 59 | sinkCount++; 60 | files.push(sinkName(outDir, sourceName, sinkCount)); 61 | datasink.flush(); 62 | datasink.close(); 63 | datasink = null; 64 | datasink = createSink(outDir, sourceName, sinkCount); 65 | } 66 | 67 | if (count === layerCount) { 68 | datasink.flush(); 69 | datasink.close(); 70 | datasink = null; 71 | } 72 | 73 | count++; 74 | }); 75 | 76 | datasource.close(); 77 | return callback(null, files); 78 | } 79 | 80 | function sinkName(out, name, count) { 81 | return path.join(out, name + '-' + count + '.kml'); 82 | } 83 | 84 | function createSink(out, name, count) { 85 | return gdal.open(sinkName(out, name, count), 'w', 'KML'); 86 | } 87 | 88 | module.exports = kmlSplit; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mapbox/kml-split", 3 | "version": "0.0.1", 4 | "description": "Split KML files with many layers into separate files", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "tape test/test.js" 8 | }, 9 | "author": "Mapbox", 10 | "license": "ISC", 11 | "dependencies": { 12 | "gdal": "^0.9.2", 13 | "yargs": "^4.8.1" 14 | }, 15 | "bin": { 16 | "kml-split": "./bin/kml-split.js" 17 | }, 18 | "devDependencies": { 19 | "rimraf": "^2.5.4", 20 | "tape": "^4.6.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/fixtures/invalid.kml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Layer 1 6 | 7 | 8 | 11.7333984375,38.61687046392973 11.7333984375,48.45835188280866 23.73046875,48.45835188280866 23.73046875,38.61687046392973 11.7333984375,38.61687046392973 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/fixtures/no-layers.kml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /test/fixtures/valid-22-layers.kml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Layer 1 6 | 7 | 8 | 11.7333984375,38.61687046392973 11.7333984375,48.45835188280866 23.73046875,48.45835188280866 23.73046875,38.61687046392973 11.7333984375,38.61687046392973 9 | 10 | 11 | 12 | Layer 2 13 | 14 | 15 | 11.7333984375,38.61687046392973 11.7333984375,48.45835188280866 23.73046875,48.45835188280866 23.73046875,38.61687046392973 11.7333984375,38.61687046392973 16 | 17 | 18 | 19 | Layer 3 20 | 21 | 22 | 11.7333984375,38.61687046392973 11.7333984375,48.45835188280866 23.73046875,48.45835188280866 23.73046875,38.61687046392973 11.7333984375,38.61687046392973 23 | 24 | 25 | 26 | Layer 4 27 | 28 | 29 | 11.7333984375,38.61687046392973 11.7333984375,48.45835188280866 23.73046875,48.45835188280866 23.73046875,38.61687046392973 11.7333984375,38.61687046392973 30 | 31 | 32 | 33 | Layer 5 34 | 35 | 36 | 11.7333984375,38.61687046392973 11.7333984375,48.45835188280866 23.73046875,48.45835188280866 23.73046875,38.61687046392973 11.7333984375,38.61687046392973 37 | 38 | 39 | 40 | Layer 6 41 | 42 | 43 | 11.7333984375,38.61687046392973 11.7333984375,48.45835188280866 23.73046875,48.45835188280866 23.73046875,38.61687046392973 11.7333984375,38.61687046392973 44 | 45 | 46 | 47 | Layer 7 48 | 49 | 50 | 11.7333984375,38.61687046392973 11.7333984375,48.45835188280866 23.73046875,48.45835188280866 23.73046875,38.61687046392973 11.7333984375,38.61687046392973 51 | 52 | 53 | 54 | Layer 8 55 | 56 | 57 | 11.7333984375,38.61687046392973 11.7333984375,48.45835188280866 23.73046875,48.45835188280866 23.73046875,38.61687046392973 11.7333984375,38.61687046392973 58 | 59 | 60 | 61 | Layer 9 62 | 63 | 64 | 11.7333984375,38.61687046392973 11.7333984375,48.45835188280866 23.73046875,48.45835188280866 23.73046875,38.61687046392973 11.7333984375,38.61687046392973 65 | 66 | 67 | 68 | Layer 10 69 | 70 | 71 | 11.7333984375,38.61687046392973 11.7333984375,48.45835188280866 23.73046875,48.45835188280866 23.73046875,38.61687046392973 11.7333984375,38.61687046392973 72 | 73 | 74 | 75 | Layer 11 76 | 77 | 78 | 11.7333984375,38.61687046392973 11.7333984375,48.45835188280866 23.73046875,48.45835188280866 23.73046875,38.61687046392973 11.7333984375,38.61687046392973 79 | 80 | 81 | 82 | Layer 12 83 | 84 | 85 | 11.7333984375,38.61687046392973 11.7333984375,48.45835188280866 23.73046875,48.45835188280866 23.73046875,38.61687046392973 11.7333984375,38.61687046392973 86 | 87 | 88 | 89 | Layer 13 90 | 91 | 92 | 11.7333984375,38.61687046392973 11.7333984375,48.45835188280866 23.73046875,48.45835188280866 23.73046875,38.61687046392973 11.7333984375,38.61687046392973 93 | 94 | 95 | 96 | Layer 14 97 | 98 | 99 | 11.7333984375,38.61687046392973 11.7333984375,48.45835188280866 23.73046875,48.45835188280866 23.73046875,38.61687046392973 11.7333984375,38.61687046392973 100 | 101 | 102 | 103 | Layer 15 104 | 105 | 106 | 11.7333984375,38.61687046392973 11.7333984375,48.45835188280866 23.73046875,48.45835188280866 23.73046875,38.61687046392973 11.7333984375,38.61687046392973 107 | 108 | 109 | 110 | Layer 16 111 | 112 | 113 | 11.7333984375,38.61687046392973 11.7333984375,48.45835188280866 23.73046875,48.45835188280866 23.73046875,38.61687046392973 11.7333984375,38.61687046392973 114 | 115 | 116 | 117 | Layer 17 118 | 119 | 120 | 11.7333984375,38.61687046392973 11.7333984375,48.45835188280866 23.73046875,48.45835188280866 23.73046875,38.61687046392973 11.7333984375,38.61687046392973 121 | 122 | 123 | 124 | Layer 18 125 | 126 | 127 | 11.7333984375,38.61687046392973 11.7333984375,48.45835188280866 23.73046875,48.45835188280866 23.73046875,38.61687046392973 11.7333984375,38.61687046392973 128 | 129 | 130 | 131 | Layer 19 132 | 133 | 134 | 11.7333984375,38.61687046392973 11.7333984375,48.45835188280866 23.73046875,48.45835188280866 23.73046875,38.61687046392973 11.7333984375,38.61687046392973 135 | 136 | 137 | 138 | Layer 20 139 | 140 | 141 | 11.7333984375,38.61687046392973 11.7333984375,48.45835188280866 23.73046875,48.45835188280866 23.73046875,38.61687046392973 11.7333984375,38.61687046392973 142 | 143 | 144 | 145 | Layer 21 146 | 147 | 148 | 11.7333984375,38.61687046392973 11.7333984375,48.45835188280866 23.73046875,48.45835188280866 23.73046875,38.61687046392973 11.7333984375,38.61687046392973 149 | 150 | 151 | 152 | Layer 22 153 | 154 | 155 | 11.7333984375,38.61687046392973 11.7333984375,48.45835188280866 23.73046875,48.45835188280866 23.73046875,38.61687046392973 11.7333984375,38.61687046392973 156 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | var clean = require('rimraf').sync; 5 | var kmlSplit = require('..'); 6 | var gdal = require('gdal'); 7 | 8 | var wgs84; 9 | var out = path.resolve(__dirname, 'output'); 10 | clean(out); 11 | 12 | test('successfully splits layers', function(t) { 13 | fs.mkdirSync(out); 14 | var kml = path.resolve(__dirname, 'fixtures', 'valid-22-layers.kml'); 15 | 16 | kmlSplit(kml, {maxLayers: 5, outDir: out}, function(err, response) { 17 | 18 | t.equal(response.length, 5, 'callback array is proper length'); 19 | 20 | response.forEach(function(f) { 21 | t.ok(fs.statSync(f), 'file written successfully') 22 | }); 23 | 24 | var datasource; 25 | try { 26 | wgs84 = gdal.SpatialReference.fromEPSG(4326); 27 | datasource = gdal.open(path.resolve(__dirname, 'output', 'valid-22-layers-1.kml')); 28 | } catch (err) { 29 | throw err; 30 | } 31 | 32 | t.equal(datasource.layers.count(), 5, 'has proper number of layers'); 33 | var count = 1; 34 | datasource.layers.forEach(function(layer) { 35 | t.equal(layer.name, 'Layer_'+count, 'layer name is correct yay'); 36 | count++; 37 | }); 38 | 39 | datasource.close(); 40 | 41 | clean(out); 42 | t.end(); 43 | }); 44 | }); 45 | 46 | test('works fine without passing options', function(t) { 47 | fs.mkdirSync(out); 48 | var kml = path.resolve(__dirname, 'fixtures', 'valid-22-layers.kml'); 49 | 50 | kmlSplit(kml, function(err, response) { 51 | t.notOk(err, 'no error'); 52 | t.ok(response, 'successfull response'); 53 | clean(out); 54 | t.end(); 55 | }); 56 | }); 57 | 58 | test('errors with invalid kml file (broken tags)', function(t) { 59 | fs.mkdirSync(out); 60 | var kml = path.resolve(__dirname, 'fixtures', 'invalid.kml'); 61 | 62 | kmlSplit(kml, {maxLayers: 5, outDir: out}, function(err, success) { 63 | t.ok(err); 64 | t.ok(err.message.indexOf('Error opening dataset') > -1, 'proper error message'); 65 | clean(out); 66 | t.end(); 67 | }); 68 | }); 69 | 70 | test('does not split if maxLayers is larger than total layers', function(t) { 71 | fs.mkdirSync(out); 72 | var kml = path.resolve(__dirname, 'fixtures', 'valid-22-layers.kml'); 73 | 74 | kmlSplit(kml, {maxLayers: 25, outDir: out}, function(err, success) { 75 | t.ok(err); 76 | t.ok(err.message.indexOf('Maximum layers is larger than layer count.') > -1, 'proper error message'); 77 | clean(out); 78 | t.end(); 79 | }); 80 | }); 81 | 82 | test('kml contains no layers', function(t) { 83 | fs.mkdirSync(out); 84 | var kml = path.resolve(__dirname, 'fixtures', 'no-layers.kml'); 85 | 86 | kmlSplit(kml, function(err, success) { 87 | t.ok(err); 88 | t.ok(err.message.indexOf('KML does not contain any layers.') > -1, 'proper error message'); 89 | clean(out); 90 | t.end(); 91 | }); 92 | }); --------------------------------------------------------------------------------