├── .eslintrc ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.txt ├── index.js ├── package.json ├── readme.md └── test.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "quotes": [2, "single"], 4 | "quote-props": [2, "as-needed"], 5 | "no-console": [1], 6 | "semi": [2, "always"], 7 | "space-before-function-paren": [2, "never"], 8 | "object-curly-spacing": [2, "always"], 9 | "array-bracket-spacing": [2, "never"], 10 | "comma-spacing": [2, { "before": false, "after": true }], 11 | "key-spacing": [2, { "beforeColon": false, "afterColon": true }] 12 | }, 13 | "env": { 14 | "node": true, 15 | "es6": true 16 | }, 17 | "globals": { 18 | "process": true, 19 | "module": true, 20 | "require": true 21 | }, 22 | "extends": "eslint:recommended" 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 4.4.2 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.3.2 2 | 3 | * Adds support of `MultiPoints`, `MultiLineStrings` and `MultiPolygons` [#28](https://github.com/mapbox/simplespec-to-gl-style/pull/28) 4 | 5 | # 0.3.1 6 | 7 | * Allow markers to overlap [#26](https://github.com/mapbox/simplespec-to-gl-style/pull/26) 8 | 9 | # 0.3.0 10 | 11 | * Adds support for lines with fill properties [#25](https://github.com/mapbox/simplespec-to-gl-style/pull/25) 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var cuid = require('cuid'); 4 | 5 | function convert(geojson) { 6 | var sourceId = cuid(); 7 | 8 | // If a LineString has fill properties, we need to add a source layer for it 9 | if (geojson.features && geojson.features.length > 0) { 10 | geojson.features = geojson.features.map(function(feature) { 11 | return checkForLineAsPolygon(feature); 12 | }); 13 | } else { 14 | geojson = checkForLineAsPolygon(geojson); 15 | } 16 | 17 | var style = { 18 | version: 8, 19 | sources: makeSource(geojson, sourceId), 20 | layers: addLayers(geojson, sourceId, []), 21 | glyphs: 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf' 22 | }; 23 | 24 | return style; 25 | } 26 | 27 | function addLayers(geojson, sourceId, layers) { 28 | switch (geojson.type) { 29 | case 'FeatureCollection': 30 | geojson.features.forEach(function(feature) { 31 | addLayers(feature, sourceId, layers); 32 | }); 33 | break; 34 | default: throw new Error('unknown or unsupported GeoJSON type'); 35 | case 'Feature': 36 | switch (geojson.geometry.type) { 37 | case 'MultiPoint': 38 | case 'Point': 39 | if (!geojson.properties) geojson.properties = {}; 40 | geojson.properties._id = cuid(); 41 | layers.push(makeLayer(geojson, sourceId, 'Point')); 42 | break; 43 | case 'MultiLineString': 44 | case 'LineString': 45 | if (!geojson.properties) geojson.properties = {}; 46 | geojson.properties._id = cuid(); 47 | layers.push(makeLayer(geojson, sourceId, 'LineString')); 48 | break; 49 | case 'MultiPolygon': 50 | case 'Polygon': 51 | if (!geojson.properties) geojson.properties = {}; 52 | geojson.properties._id = cuid(); 53 | layers.push(makeLayer(geojson, sourceId, 'LineString')); 54 | layers.push(makeLayer(geojson, sourceId, 'Polygon')); 55 | break; 56 | default: throw new Error('unknown or unsupported GeoJSON geometry type'); 57 | } 58 | } 59 | return layers; 60 | } 61 | 62 | function makeLayer(feature, sourceId, geometry) { 63 | var layer; 64 | if (geometry === 'Point') { 65 | layer = { 66 | source: sourceId, 67 | id: cuid(), 68 | type: 'symbol', 69 | layout: { 70 | 'icon-image': feature.properties._id, 71 | 'icon-size': 1, 72 | 'icon-allow-overlap': true, 73 | 'icon-ignore-placement': true 74 | }, 75 | filter: [ 76 | '==', 77 | '_id', 78 | feature.properties._id 79 | ] 80 | }; 81 | } else if (geometry === 'LineString') { 82 | layer = { 83 | type: 'line', 84 | source: sourceId, 85 | id: cuid(), 86 | paint: { 87 | 'line-color': 'stroke' in feature.properties ? feature.properties.stroke : '#555555', 88 | 'line-opacity': 'stroke-opacity' in feature.properties ? +feature.properties['stroke-opacity'] : 1.0, 89 | 'line-width': 'stroke-width' in feature.properties ? +feature.properties['stroke-width'] : 2 90 | }, 91 | layout: { 92 | 'line-cap': 'round', 93 | 'line-join': 'bevel' 94 | }, 95 | filter: [ 96 | '==', 97 | '_id', 98 | feature.properties._id 99 | ] 100 | }; 101 | } else if (geometry === 'Polygon') { 102 | layer = { 103 | type: 'fill', 104 | source: sourceId, 105 | id: cuid(), 106 | paint: { 107 | 'fill-color': 'fill' in feature.properties ? feature.properties.fill : '#555555', 108 | 'fill-opacity': 'fill-opacity' in feature.properties ? +feature.properties['fill-opacity'] : 0.5 109 | }, 110 | filter: [ 111 | '==', 112 | '_id', 113 | feature.properties._id 114 | ] 115 | }; 116 | } 117 | return layer; 118 | } 119 | 120 | function checkForLineAsPolygon(feature) { 121 | if (feature.properties && feature.geometry.type === 'LineString' && ('fill' in feature.properties || 'fill-opacity' in feature.properties)) { 122 | let coordinates = feature.geometry.coordinates; 123 | 124 | // A polygon needs to have the the same first and last coordinates 125 | coordinates.push(coordinates[0]); 126 | 127 | return { 128 | type: 'Feature', 129 | properties: feature.properties || {}, 130 | geometry: { 131 | type: 'Polygon', 132 | coordinates: [ 133 | coordinates 134 | ] 135 | } 136 | }; 137 | } else { 138 | return feature; 139 | } 140 | } 141 | 142 | function makeSource(geojson, sourceId) { 143 | var sources = {}; 144 | sources[sourceId] = { 145 | type: 'geojson', 146 | data: geojson 147 | }; 148 | return sources; 149 | } 150 | 151 | module.exports = convert; 152 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mapbox/simplespec-to-gl-style", 3 | "version": "0.4.0", 4 | "description": "Converts GeoJSON styled with simplestyle-spec to a GL Style", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "eslint index.js && tape ./test.js" 8 | }, 9 | "author": "Bobby Sudekum", 10 | "license": "ISC", 11 | "dependencies": { 12 | "cuid": "^1.3.8" 13 | }, 14 | "devDependencies": { 15 | "eslint": "^3.1.1", 16 | "tape": "^4.2.2" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## simplespec-to-gl-style 2 | 3 | Converts GeoJSON styled with [simplestyle-spec](https://github.com/mapbox/simplestyle-spec/) to a GL Style 4 | 5 | ## Install 6 | 7 | ``` 8 | npm install @mapbox/simplespec-to-gl-style --save 9 | ``` 10 | 11 | #### Usage: 12 | 13 | ```js 14 | var convert = require('@mapbox/simplespec-to-gl-style'); 15 | 16 | var style = convert(myGeoJSON); 17 | 18 | var map = new mapboxgl.Map({ 19 | container: 'map', 20 | style: style, // add style to a map 21 | center: [-74.50, 40], 22 | zoom: 9 23 | }); 24 | ``` 25 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | var simpleToGL = require('./index.js'); 3 | 4 | /* eslint-disable */ 5 | var validFeatureCollection = {"type":"FeatureCollection","features":[{"type":"Feature","properties":{"stroke":"#3eb367","stroke-width":2,"stroke-opacity":1},"geometry":{"type":"LineString","coordinates":[[40.78125,57.32652122521709],[10.546875,41.244772343082076],[57.65624999999999,18.312810846425442]]}},{"type":"Feature","properties":{"stroke":"#5bfa35","stroke-width":2,"stroke-opacity":1,"fill":"#b87acb","fill-opacity":0.5},"geometry":{"type":"Polygon","coordinates":[[[-23.5546875,23.88583769986199],[-31.640625,-8.05922962720018],[8.7890625,-8.05922962720018],[-23.5546875,23.88583769986199]]]}}]}; 6 | var validFeatureCollectionWithNoProperties = {"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"LineString","coordinates":[[40.78125,57.32652122521709],[10.546875,41.244772343082076],[57.65624999999999,18.312810846425442]]}},{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-23.5546875,23.88583769986199],[-31.640625,-8.05922962720018],[8.7890625,-8.05922962720018],[-23.5546875,23.88583769986199]]]}}]}; 7 | var invalidFeatureCollection = {"type":"FeatureCollection","foobar":[{"type":"Feature","properties":{"stroke":"#3eb367","stroke-width":2,"stroke-opacity":1},"geometry":{"type":"LineString","coordinates":[[40.78125,57.32652122521709],[10.546875,41.244772343082076],[57.65624999999999,18.312810846425442]]}},{"type":"Feature","properties":{"stroke":"#5bfa35","stroke-width":2,"stroke-opacity":1,"fill":"#b87acb","fill-opacity":0.5},"geometry":{"type":"Polygon","coordinates":[[[-23.5546875,23.88583769986199],[-31.640625,-8.05922962720018],[8.7890625,-8.05922962720018],[-23.5546875,23.88583769986199]]]}}]}; 8 | var invalidFeatureCollectionType = {"type":"foobar","features":[{"type":"Feature","properties":{"stroke":"#3eb367","stroke-width":2,"stroke-opacity":1},"geometry":{"type":"LineString","coordinates":[[40.78125,57.32652122521709],[10.546875,41.244772343082076],[57.65624999999999,18.312810846425442]]}},{"type":"Feature","properties":{"stroke":"#5bfa35","stroke-width":2,"stroke-opacity":1,"fill":"#b87acb","fill-opacity":0.5},"geometry":{"type":"Polygon","coordinates":[[[-23.5546875,23.88583769986199],[-31.640625,-8.05922962720018],[8.7890625,-8.05922962720018],[-23.5546875,23.88583769986199]]]}}]}; 9 | var point = {"type":"FeatureCollection","features":[{"type":"Feature","properties":{},"geometry":{"type":"Point","coordinates":[15.8203125,28.613459424004414]}}]}; 10 | var pointWithImageAndSize = {"type":"FeatureCollection","features":[{"type":"Feature","properties":{"marker-color":"#7e7e7e","marker-symbol":"airport","marker-size":"large"},"geometry":{"type":"Point","coordinates":[28.4765625,16.63619187839765]}}]}; 11 | var singleGeoJSONFeature = {"type":"Feature","properties":{},"geometry":{"type":"Polygon","coordinates":[[[36.2109375,60.06484046010452],[-4.921875,47.98992166741417],[14.0625,3.8642546157214213],[61.52343749999999,16.63619187839765],[36.2109375,60.06484046010452]]]}}; 12 | var multiPolygon = {"type":"FeatureCollection","features":[{"type":"Feature","properties":{},"geometry":{"type":"MultiPolygon","coordinates":[[[[-84.71239,39.052056],[-84.368778,39.052056],[-84.368778,39.221037],[-84.71239,39.221037],[-84.71239,39.052056]]]]}}]}; 13 | /* eslint-enable */ 14 | 15 | test('valid', function(t) { 16 | var style = simpleToGL(validFeatureCollection); 17 | t.deepEqual(style.version, 8, 'Version should be 8'); 18 | 19 | for (var key in style.sources) { 20 | var source = style.sources[key]; 21 | for (var i = 0; i < source.data.features.length; i++) { 22 | t.deepEqual(style.layers[i].filter[2], source.data.features[i].properties._id, 'ids match between geojson source and layer'); 23 | } 24 | } 25 | 26 | t.deepEqual(style.layers.length, 3, 'Should create 3 layers: 1 for LineString, and 2 for polygon'); 27 | 28 | t.deepEqual(style.layers[0].paint['line-color'], validFeatureCollection.features[0].properties['stroke'], 'LineString line-color the same'); 29 | t.deepEqual(style.layers[0].paint['line-width'], validFeatureCollection.features[0].properties['stroke-width'], 'LineString line-width the same'); 30 | t.deepEqual(style.layers[0].paint['line-opacity'], validFeatureCollection.features[0].properties['stroke-opacity'], 'LineString line-opacity the same'); 31 | 32 | t.deepEqual(style.layers[1].paint['line-color'], validFeatureCollection.features[1].properties['stroke'], 'LineString line-color the same'); 33 | t.deepEqual(style.layers[1].paint['line-width'], validFeatureCollection.features[1].properties['stroke-width'], 'LineString line-width the same'); 34 | t.deepEqual(style.layers[1].paint['line-opacity'], validFeatureCollection.features[1].properties['stroke-opacity'], 'LineString line-opacity the same'); 35 | 36 | t.deepEqual(style.layers[2].paint['fill-color'], validFeatureCollection.features[1].properties['fill'], 'Polyong fill-color the same'); 37 | t.deepEqual(style.layers[2].paint['fill-opacity'], validFeatureCollection.features[1].properties['fill-opacity'], 'Polyong fill-opacity the same'); 38 | 39 | t.deepEqual(style.layers[0].source, style.layers[1].source, 'Sources should be the same'); 40 | t.end(); 41 | }); 42 | 43 | test('valid with no properties', function(t) { 44 | var style = simpleToGL(validFeatureCollectionWithNoProperties); 45 | t.deepEqual(style.version, 8, 'Version should be 8'); 46 | 47 | for (var key in style.sources) { 48 | var source = style.sources[key]; 49 | for (var i = 0; i < source.data.features.length; i++) { 50 | t.deepEqual(style.layers[i].filter[2], source.data.features[i].properties._id, 'ids match between geojson source and layer'); 51 | } 52 | } 53 | 54 | t.deepEqual(style.layers[0].paint['line-color'], '#555555', 'LineString line-color is default value'); 55 | t.deepEqual(style.layers[0].paint['line-opacity'], 1.0, 'LineString line-opacity is default value'); 56 | t.deepEqual(style.layers[0].paint['line-width'], 2.0, 'LineString line-width is default value'); 57 | 58 | t.deepEqual(style.layers[1].paint['line-color'], '#555555', 'LineString line-color is default value'); 59 | t.deepEqual(style.layers[1].paint['line-opacity'], 1.0, 'LineString line-opacity is default value'); 60 | t.deepEqual(style.layers[1].paint['line-width'], 2.0, 'LineString line-width is default value'); 61 | 62 | t.deepEqual(style.layers[2].paint['fill-color'], '#555555', 'Polyong fill-color is default value'); 63 | t.deepEqual(style.layers[2].paint['fill-opacity'], 0.5, 'Polyong fill-opacity is default value'); 64 | 65 | t.end(); 66 | }); 67 | 68 | test('valid multiPolygon no properties', function(t) { 69 | var style = simpleToGL(multiPolygon); 70 | t.deepEqual(style.version, 8, 'Version should be 8'); 71 | 72 | for (var key in style.sources) { 73 | var source = style.sources[key]; 74 | for (var i = 0; i < source.data.features.length; i++) { 75 | t.deepEqual(style.layers[i].filter[2], source.data.features[i].properties._id, 'ids match between geojson source and layer'); 76 | } 77 | } 78 | 79 | t.deepEqual(style.layers[0].paint['line-color'], '#555555', 'LineString line-color is default value'); 80 | t.deepEqual(style.layers[0].paint['line-opacity'], 1.0, 'LineString line-opacity is default value'); 81 | t.deepEqual(style.layers[0].paint['line-width'], 2.0, 'LineString line-width is default value'); 82 | 83 | t.deepEqual(style.layers[1].paint['fill-color'], '#555555', 'Polygon fill-color is default value'); 84 | t.deepEqual(style.layers[1].paint['fill-opacity'], 0.5, 'Polygon fill-opacity is default value'); 85 | 86 | t.end(); 87 | }); 88 | 89 | test('valid single feature', function(t) { 90 | var style = simpleToGL(singleGeoJSONFeature); 91 | t.deepEqual(style.version, 8, 'Version should be 8'); 92 | 93 | for (var key in style.sources) { 94 | var source = style.sources[key]; 95 | for (var i = 0; i < style.layers.length; i++) { 96 | t.deepEqual(style.layers[i].filter[2], source.data.properties._id, 'ids match between geojson source and layer'); 97 | } 98 | } 99 | 100 | t.deepEqual(style.layers[0].paint['line-color'], '#555555', 'LineString line-color is default value'); 101 | t.deepEqual(style.layers[0].paint['line-opacity'], 1.0, 'LineString line-opacity is default value'); 102 | t.deepEqual(style.layers[0].paint['line-width'], 2.0, 'LineString line-width is default value'); 103 | 104 | t.deepEqual(style.layers[1].paint['fill-color'], '#555555', 'Polyong fill-color is default value'); 105 | t.deepEqual(style.layers[1].paint['fill-opacity'], 0.5, 'Polyong fill-opacity is default value'); 106 | 107 | t.end(); 108 | }); 109 | 110 | test('valid single point defaults to point', function(t) { 111 | var style = simpleToGL(point); 112 | t.deepEqual(style.version, 8, 'Version should be 8'); 113 | t.deepEqual(style.layers[0].type, 'symbol', 'Default symbol'); 114 | t.deepEqual(style.layers[0].layout['icon-size'], 1); 115 | t.end(); 116 | }); 117 | 118 | test('valid single point with image', function(t) { 119 | var style = simpleToGL(pointWithImageAndSize); 120 | t.deepEqual(style.version, 8, 'Version should be 8'); 121 | t.deepEqual(style.layers[0].type, 'symbol'); 122 | t.ok(style.layers[0].layout['icon-image'], 'Custom marker'); 123 | t.ok(style.layers[0].layout['icon-allow-overlap'], 'Allow marker overlap'); 124 | t.deepEqual(style.layers[0].layout['icon-size'], 1, 'Custom size'); 125 | 126 | t.end(); 127 | }); 128 | 129 | test('invalid image size defaults to 1', function(t) { 130 | var invalid = {"type":"FeatureCollection","features":[{"type":"Feature","properties":{"marker-color":"#7e7e7e","marker-symbol":"airport","marker-size":"super-large"},"geometry":{"type":"Point","coordinates":[28.4765625,16.63619187839765]}}]}; // eslint-disable-line 131 | var style = simpleToGL(invalid); 132 | t.deepEqual(style.version, 8, 'Version should be 8'); 133 | t.deepEqual(style.layers[0].layout['icon-size'], 1, 'Default size'); 134 | t.end(); 135 | }); 136 | 137 | test('invalid FeatureCollection', function(t) { 138 | t.throws(function() { 139 | simpleToGL(invalidFeatureCollectionType); 140 | }, { 141 | message: 'unknown or unsupported GeoJSON type' 142 | }, 'Invalid GeoJSON type'); 143 | t.end(); 144 | }); 145 | 146 | 147 | test('LineString with fill properties', function(t) { 148 | var geojson = {"type":"FeatureCollection","features":[{"type":"Feature","properties":{"stroke-width":"2","stroke":"#000000","stroke-opacity":"1.0","fill":"#ff0000","fill-opacity":"0.25"},"geometry":{"type":"LineString","coordinates":[[-122.68209,45.52475],[-122.67488,45.52451],[-122.67608,45.51681],[-122.68998,45.51693],[-122.68964,45.5203],[-122.68209,45.52475]]}}]}; // eslint-disable-line 149 | var style = simpleToGL(geojson); 150 | 151 | t.equal(style.layers.length, 2, 'Has two layers'); 152 | t.equal(style.layers[1].type, 'fill'); 153 | t.equal(style.layers[1].paint['fill-color'], '#ff0000'); 154 | t.equal(style.layers[1].paint['fill-opacity'], 0.25); 155 | t.end(); 156 | }); 157 | --------------------------------------------------------------------------------