├── .gitignore ├── .npmignore ├── test ├── data │ ├── rocket.png │ ├── mountain.jpg │ ├── too-large.png │ ├── dedup.geojson.png │ ├── example.geojson.png │ ├── point.geojson.png │ ├── stroked.geojson.png │ ├── hashless.geojson.png │ ├── multipoint.geojson.png │ ├── point-retina.geojson.png │ ├── example-retina.geojson.png │ ├── feature-nullproperties.geojson.png │ ├── feature-nullproperties.geojson │ ├── point.geojson │ ├── point-retina.geojson │ ├── multipoint.geojson │ ├── example.geojson │ ├── example-retina.geojson │ ├── hashless.geojson │ ├── dedup.geojson │ ├── feature-nullgeom.xml │ ├── stroked.geojson │ ├── point.xml │ ├── point-retina.xml │ ├── feature-nullproperties.xml │ ├── multipoint.xml │ ├── example.xml │ ├── example-retina.xml │ ├── hashless.xml │ ├── stroked.xml │ ├── dedup.xml │ └── html-page.png ├── defaults.js ├── normalizestyle.js └── test.js ├── lib ├── constants.js ├── cachepath.js ├── defaults.js ├── template.xml ├── normalizestyle.js ├── urlmarker.js └── get.js ├── .travis.yml ├── bin └── geojson-mapnikify ├── LICENSE.txt ├── CHANGELOG.md ├── package.json ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /test 2 | /node_modules 3 | -------------------------------------------------------------------------------- /test/data/rocket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wmde/geojson-mapnikify/master/test/data/rocket.png -------------------------------------------------------------------------------- /test/data/mountain.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wmde/geojson-mapnikify/master/test/data/mountain.jpg -------------------------------------------------------------------------------- /test/data/too-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wmde/geojson-mapnikify/master/test/data/too-large.png -------------------------------------------------------------------------------- /test/data/dedup.geojson.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wmde/geojson-mapnikify/master/test/data/dedup.geojson.png -------------------------------------------------------------------------------- /test/data/example.geojson.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wmde/geojson-mapnikify/master/test/data/example.geojson.png -------------------------------------------------------------------------------- /test/data/point.geojson.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wmde/geojson-mapnikify/master/test/data/point.geojson.png -------------------------------------------------------------------------------- /test/data/stroked.geojson.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wmde/geojson-mapnikify/master/test/data/stroked.geojson.png -------------------------------------------------------------------------------- /test/data/hashless.geojson.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wmde/geojson-mapnikify/master/test/data/hashless.geojson.png -------------------------------------------------------------------------------- /test/data/multipoint.geojson.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wmde/geojson-mapnikify/master/test/data/multipoint.geojson.png -------------------------------------------------------------------------------- /test/data/point-retina.geojson.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wmde/geojson-mapnikify/master/test/data/point-retina.geojson.png -------------------------------------------------------------------------------- /test/data/example-retina.geojson.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wmde/geojson-mapnikify/master/test/data/example-retina.geojson.png -------------------------------------------------------------------------------- /test/data/feature-nullproperties.geojson.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wmde/geojson-mapnikify/master/test/data/feature-nullproperties.geojson.png -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is used as a reference for services built on this code. 3 | * Consult the support and sales team before modifying any values. 4 | */ 5 | module.exports = { 6 | MARKER_MAX_SQUARE_PIXELS: 160000 7 | }; 8 | -------------------------------------------------------------------------------- /test/data/feature-nullproperties.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "geometry": { 4 | "type": "Point", 5 | "coordinates": [125.6, 10.1] 6 | }, 7 | "properties": { 8 | "name": "Dinagat Islands" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/data/point.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "geometry": { 4 | "type": "Point", 5 | "coordinates": [125.6, 10.1] 6 | }, 7 | "properties": { 8 | "name": "Dinagat Islands" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/data/point-retina.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "geometry": { 4 | "type": "Point", 5 | "coordinates": [125.6, 10.1] 6 | }, 7 | "properties": { 8 | "name": "Dinagat Islands" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/data/multipoint.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "geometry": { 4 | "type": "MultiPoint", 5 | "coordinates": [ [0, 0], [0.5, 0.5], [1, 1], [0, 1], [1, 0] ] 6 | }, 7 | "properties": { 8 | "name": "Dinagat Islands" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 6 5 | - 8 6 | 7 | sudo: false 8 | 9 | addons: 10 | apt: 11 | sources: 12 | - ubuntu-toolchain-r-test 13 | packages: 14 | - libstdc++6 # upgrade libstdc++ on linux to support C++11 15 | 16 | after_success: 17 | - npm run coverage 18 | -------------------------------------------------------------------------------- /lib/cachepath.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto'), 2 | tmpdir = require('os').tmpdir(), 3 | path = require('path'); 4 | 5 | module.exports = function(str) { 6 | var md5 = crypto.createHash('md5'); 7 | md5.update(str); 8 | return path.join(tmpdir, 'geojson-mapnikify-' + md5.digest('hex')); 9 | }; 10 | -------------------------------------------------------------------------------- /bin/geojson-mapnikify: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var generateXML = require('../'), 4 | argv = require('minimist')(process.argv.slice(2)), 5 | fs = require('fs'); 6 | 7 | if (!argv._.length) { 8 | return console.error('usage: geojson-mapnikify file.geojson [--retina] [--retina] > stylesheet.xml'); 9 | } 10 | 11 | var TMP = '/tmp/tl-overlay'; 12 | var gj = JSON.parse(fs.readFileSync(argv._[0], 'utf8')); 13 | var gen = generateXML(gj, argv.retina, function(err, res) { 14 | if (err) return console.error(err); 15 | if (argv.normalize) { 16 | process.stdout.write(normalize(res)); 17 | } else { 18 | process.stdout.write(res); 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /test/data/example.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [{ "type": "Feature", 4 | "geometry": { 5 | "type": "Point", 6 | "coordinates": [0, 0] 7 | }, 8 | "properties": { 9 | "marker-size": "medium", 10 | "marker-symbol": "bus", 11 | "marker-color": "#ace" 12 | } 13 | }, { 14 | "type": "Feature", 15 | "geometry": { 16 | "type": "LineString", 17 | "coordinates": [[0, 0], [10, 10]] 18 | }, 19 | "properties": { 20 | "stroke": "#f0f0f0", 21 | "stroke-width": 2 22 | } 23 | }] 24 | } 25 | -------------------------------------------------------------------------------- /test/data/example-retina.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [{ "type": "Feature", 4 | "geometry": { 5 | "type": "Point", 6 | "coordinates": [0, 0] 7 | }, 8 | "properties": { 9 | "marker-size": "medium", 10 | "marker-symbol": "bus", 11 | "marker-color": "#ace" 12 | } 13 | }, { 14 | "type": "Feature", 15 | "geometry": { 16 | "type": "LineString", 17 | "coordinates": [[0, 0], [10, 10]] 18 | }, 19 | "properties": { 20 | "stroke": "#f0f0f0", 21 | "stroke-width": 2 22 | } 23 | }] 24 | } 25 | -------------------------------------------------------------------------------- /test/data/hashless.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [{ "type": "Feature", 4 | "geometry": { 5 | "type": "Point", 6 | "coordinates": [0, 0] 7 | }, 8 | "properties": { 9 | "marker-size": "medium", 10 | "marker-symbol": "bus", 11 | "marker-color": "ace" 12 | } 13 | }, { 14 | "type": "Feature", 15 | "geometry": { 16 | "type": "LineString", 17 | "coordinates": [[0, 0], [10, 10]] 18 | }, 19 | "properties": { 20 | "stroke": "0f0", 21 | "stroke-width": 10, 22 | "stroke-opacity": 0.5, 23 | "fill": "7e7e7e" 24 | } 25 | }] 26 | } 27 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## changelog 2 | 3 | ## 2.1.0 4 | 5 | - Throw error with description if url marker is too large. 6 | 7 | ## 2.0.0 8 | 9 | - Update to @mapbox/makizushi ^3.0.1 10 | - Update to @mapbox/blend ^2.0.1 11 | - Update to mapnik 3.x || 4.x 12 | 13 | ## 1.0.0 14 | 15 | - Update to mapnik 3.7.0 16 | - Update to blend 2.0.0 17 | - Update to makizushi 2.0.0 18 | - Drops windows support 19 | 20 | ### 0.8.0 21 | 22 | - Update to mapnik 3.6.0 23 | 24 | ### 0.7.1 25 | 26 | - Fix bug which tinted all url markers to #7e7e7e unless `marker-color` was specified. 27 | 28 | ### 0.7.0 29 | 30 | - Updated mapnik to 3.5.0 31 | 32 | ### 0.5.0 33 | 34 | - Updates node-agentkeepalive dependency to v2.0.2. This is a breaking change for node 0.8.x users. 35 | 36 | ### 0.4.3 37 | 38 | - Fixed handling of multipoint geometries 39 | -------------------------------------------------------------------------------- /lib/defaults.js: -------------------------------------------------------------------------------- 1 | var xtend = require('xtend'); 2 | 3 | var defaultFilled = { 4 | fill: '#555555', 5 | 'fill-opacity': 0.6, 6 | stroke: '#555555', 7 | 'stroke-width': 2, 8 | 'stroke-opacity': 1 9 | }; 10 | 11 | var defaultStroked = { 12 | stroke: '#555555', 13 | 'stroke-width': 2, 14 | 'stroke-opacity': 1 15 | }; 16 | 17 | var defaultPoint = { 18 | 'marker-color': '7e7e7e', 19 | 'marker-size': 'medium', 20 | 'symbol': '-' 21 | }; 22 | 23 | var typed = { 24 | LineString: defaultStroked, 25 | MultiLineString: defaultStroked, 26 | Polygon: defaultFilled, 27 | MultiPolygon: defaultFilled, 28 | Point: defaultPoint, 29 | MultiPoint: defaultPoint 30 | }; 31 | 32 | module.exports = enforceDefaults; 33 | 34 | function enforceDefaults(feature) { 35 | if (!feature || !feature.properties || !feature.geometry) { 36 | return feature; 37 | } 38 | var def = typed[feature.geometry.type]; 39 | feature.properties = xtend({}, def, feature.properties); 40 | return feature; 41 | } 42 | -------------------------------------------------------------------------------- /lib/template.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 22 | 23 | geoms 24 | points 25 | 26 | geojson 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /test/data/dedup.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [{ "type": "Feature", 4 | "geometry": { 5 | "type": "Point", 6 | "coordinates": [0, 0] 7 | }, 8 | "properties": { 9 | "marker-size": "medium", 10 | "marker-symbol": "bus", 11 | "marker-color": "#ace" 12 | } 13 | }, { "type": "Feature", 14 | "geometry": { 15 | "type": "Point", 16 | "coordinates": [10, 0] 17 | }, 18 | "properties": { 19 | "marker-size": "medium", 20 | "marker-symbol": "bus", 21 | "marker-color": "#ac0" 22 | } 23 | }, { "type": "Feature", 24 | "geometry": { 25 | "type": "Point", 26 | "coordinates": [10, 10] 27 | }, 28 | "properties": { 29 | "marker-size": "medium", 30 | "marker-symbol": "bus", 31 | "marker-color": "#ace" 32 | } 33 | }, { 34 | "type": "Feature", 35 | "geometry": { 36 | "type": "LineString", 37 | "coordinates": [[0, 0], [10, 10]] 38 | }, 39 | "properties": { 40 | "stroke": "#f0f0f0", 41 | "stroke-width": 2 42 | } 43 | }] 44 | } 45 | -------------------------------------------------------------------------------- /test/data/feature-nullgeom.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 22 | 23 | geoms 24 | points 25 | 26 | geojson 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /test/data/stroked.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [{ "type": "Feature", 4 | "geometry": { 5 | "type": "Point", 6 | "coordinates": [0, 0] 7 | }, 8 | "properties": { 9 | "marker-size": "medium", 10 | "marker-symbol": "bus", 11 | "marker-color": "#ace" 12 | } 13 | }, { 14 | "type": "Feature", 15 | "geometry": { 16 | "type": "LineString", 17 | "coordinates": [[0, 0], [10, 10]] 18 | }, 19 | "properties": { 20 | "stroke": "#0f0", 21 | "stroke-width": 10, 22 | "stroke-opacity": 0.5, 23 | "fill": "#7e7e7e" 24 | } 25 | }, { 26 | "type": "Feature", 27 | "geometry": { 28 | "type": "LineString", 29 | "coordinates": [[5, 0], [15, 10]] 30 | }, 31 | "properties": { 32 | "stroke": "#0ff", 33 | "stroke-width": 10, 34 | "fill": "#7e7e7e" 35 | } 36 | }, { 37 | "type": "Feature", 38 | "geometry": { 39 | "type": "Polygon", 40 | "coordinates": [[[5, 0], [15, 10], [20, 20], [5, 0]]] 41 | }, 42 | "properties": { 43 | "stroke": "#0ff", 44 | "stroke-width": 5, 45 | "fill": "#fff", 46 | "fill-opacity": 0.5 47 | } 48 | }] 49 | } 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kartotherian/geojson-mapnikify", 3 | "version": "2.1.0", 4 | "description": "transform geojson with simplestyle-spec into mapnik xml", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "tape test/*.js", 8 | "coverage": "istanbul cover tape test/*.js && coveralls < ./coverage/lcov.info" 9 | }, 10 | "publishConfig": { 11 | "access": "public" 12 | }, 13 | "bin": { 14 | "geojson-mapnikify": "./bin/geojson-mapnikify" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git://github.com/mapbox/geojson-mapnikify.git" 19 | }, 20 | "keywords": [ 21 | "geojson", 22 | "mapnik", 23 | "xml", 24 | "simplestyle" 25 | ], 26 | "author": "Tom MacWright", 27 | "license": "ISC", 28 | "bugs": { 29 | "url": "https://github.com/mapbox/geojson-mapnikify/issues" 30 | }, 31 | "homepage": "https://github.com/mapbox/geojson-mapnikify", 32 | "dependencies": { 33 | "@mapbox/geojson-normalize": "~0.0.1", 34 | "queue-async": "~1.0.7", 35 | "agentkeepalive": "~2.0.2", 36 | "request": "~2.78.0", 37 | "@kartotherian/makizushi": "^3.0.1", 38 | "minimist": "~1.2.0", 39 | "sigmund": "~1.0.0", 40 | "xtend": "~4.0.0", 41 | "@kartotherian/blend": "^2.0.1" 42 | }, 43 | "devDependencies": { 44 | "coveralls": "~2.11.1", 45 | "glob": "~4.0.5", 46 | "istanbul": "~0.3.0", 47 | "@kartotherian/mapnik": "3.7.3", 48 | "nock": "<=9.2.3", 49 | "tape": "~2.14.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/data/point.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 22 | 23 | geoms 24 | points 25 | 26 | geojson 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /test/data/point-retina.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 22 | 23 | geoms 24 | points 25 | 26 | geojson 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /test/data/feature-nullproperties.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 22 | 23 | geoms 24 | points 25 | 26 | geojson 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /test/data/multipoint.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 22 | 23 | geoms 24 | points 25 | 26 | geojson 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /test/data/example.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 22 | 23 | geoms 24 | points 25 | 26 | geojson 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /test/data/example-retina.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 22 | 23 | geoms 24 | points 25 | 26 | geojson 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /test/data/hashless.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 22 | 23 | geoms 24 | points 25 | 26 | geojson 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /test/data/stroked.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 22 | 23 | geoms 24 | points 25 | 26 | geojson 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /lib/normalizestyle.js: -------------------------------------------------------------------------------- 1 | module.exports = normalizeStyle; 2 | 3 | var hexcolor = /^#?([0-f]{3}|[0-f]{6})$/; 4 | var markersize = /^(small|medium|large)$/ 5 | 6 | var typed = { 7 | LineString: normalizeFilled, 8 | MultiLineString: normalizeFilled, 9 | Polygon: normalizeFilled, 10 | MultiPolygon: normalizeFilled, 11 | Point: normalizePoint 12 | }; 13 | 14 | function normalizeStyle(feature) { 15 | if (!feature || !feature.properties || !feature.geometry || !typed[feature.geometry.type]) { 16 | return feature; 17 | } 18 | feature.properties = typed[feature.geometry.type](feature.properties); 19 | return feature; 20 | } 21 | 22 | function normalizeFilled(properties) { 23 | if (!hexcolor.test(properties.stroke)) delete properties.stroke; 24 | if (properties.stroke && properties.stroke[0] !== '#') properties.stroke = '#' + properties.stroke; 25 | if (isNaN(parseFloat(properties['stroke-width']))) { 26 | delete properties['stroke-width']; 27 | } else { 28 | properties['stroke-width'] = parseFloat(properties['stroke-width']); 29 | } 30 | if (isNaN(parseFloat(properties['stroke-opacity']))) { 31 | delete properties['stroke-opacity']; 32 | } else { 33 | properties['stroke-opacity'] = parseFloat(properties['stroke-opacity']); 34 | } 35 | 36 | if (!hexcolor.test(properties.fill)) delete properties.fill; 37 | if (properties.fill && properties.fill[0] !== '#') properties.fill = '#' + properties.fill; 38 | if (isNaN(parseFloat(properties['fill-opacity']))) { 39 | delete properties['fill-opacity']; 40 | } else { 41 | properties['fill-opacity'] = parseFloat(properties['fill-opacity']); 42 | } 43 | 44 | return properties; 45 | } 46 | 47 | function normalizePoint(properties) { 48 | if (!markersize.test(properties['marker-size'])) delete properties['marker-size']; 49 | if (!hexcolor.test(properties['marker-color'])) delete properties['marker-color']; 50 | if (properties['marker-color'] && properties['marker-color'][0] !== '#') properties['marker-color'] = '#' + properties['marker-color']; 51 | return properties; 52 | } 53 | 54 | -------------------------------------------------------------------------------- /test/data/dedup.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 22 | 23 | geoms 24 | points 25 | 26 | geojson 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /lib/urlmarker.js: -------------------------------------------------------------------------------- 1 | var get = require('./get'), 2 | constants = require('./constants'), 3 | blend = require('@kartotherian/blend'); 4 | 5 | /** 6 | * Load an image from a URL for use as a custom marker. 7 | * 8 | * @param {object} options 9 | * @param {function} callback 10 | */ 11 | module.exports = function(feature, callback) { 12 | if (!feature.properties) return callback(new Error('Invalid feature')); 13 | var uri = feature.properties['marker-url']; 14 | var tint = feature.properties['marker-color']; 15 | 16 | if (tint) { 17 | // Remove the "#" added by normalizeStyle 18 | tint = tint.substring(1); 19 | 20 | // Expand hex shorthand (3 chars) to 6, e.g. 333 => 333333. 21 | // This is not done upstream in `node-tint` as some such 22 | // shorthand cannot be disambiguated from other tintspec strings, 23 | // e.g. 123 (rgb shorthand) vs. 123 (hue). 24 | if (tint.length === 3) tint = 25 | tint[0] + tint[0] + 26 | tint[1] + tint[1] + 27 | tint[2] + tint[2]; 28 | var parsedTint = blend.parseTintString(tint); 29 | } 30 | 31 | if (uri.substring(0, 4) !== 'http') { 32 | uri = 'http://' + uri; 33 | } 34 | 35 | get(uri, function(err, data) { 36 | if (err) return callback(err); 37 | 38 | // Check for PNG header. 39 | if (data.toString('binary', 0, 8) !== '\x89\x50\x4E\x47\x0D\x0A\x1A\x0A') { 40 | return callback({ 41 | message: 'Marker image format is not supported.', 42 | status: 415 43 | }); 44 | } 45 | 46 | // Extract width and height from the IHDR. The IHDR chunk must appear 47 | // first, so the location is always fixed. 48 | var width = data.readUInt32BE(16), 49 | height = data.readUInt32BE(20); 50 | 51 | // Check image size. 400x400 square limit. 52 | if (width * height > constants.MARKER_MAX_SQUARE_PIXELS) { 53 | return callback({ 54 | message: 'Marker image size must not exceed ' + constants.MARKER_MAX_SQUARE_PIXELS + ' pixels.', 55 | status: 415 56 | }); 57 | } 58 | 59 | if (!parsedTint) { 60 | return callback(null, data); 61 | } 62 | 63 | blend([{ 64 | buffer:data, 65 | tint: parsedTint 66 | }], {}, function(err, tinted) { 67 | if (err) return callback(err); 68 | return callback(null, tinted); 69 | }); 70 | }); 71 | }; 72 | -------------------------------------------------------------------------------- /lib/get.js: -------------------------------------------------------------------------------- 1 | var AgentKeepAlive = require('agentkeepalive'), 2 | xtend = require('xtend'), 3 | requestLib = require('request'); 4 | 5 | // Use a single agent for all requests so that requests to the same 6 | // host can use keep-alive for performance. 7 | var agent = new AgentKeepAlive({ 8 | maxSockets: 128, 9 | maxKeepAliveRequests: 0, 10 | maxKeepAliveTime: 30000 11 | }); 12 | 13 | /** 14 | * Safely get a file at a uri as a binary buffer, with timeout protection 15 | * and a length limit. 16 | * 17 | * @param {string} uri 18 | * @param {function} callback 19 | */ 20 | module.exports = function get(options, callback) { 21 | var request = getClient(); 22 | var params = normalizeParams(options); 23 | if(!params) { callback(new Error('Invalid parameters: ' + JSON.stringify(options))); } 24 | 25 | // This might load the same file multiple times, but the overhead should 26 | // be very little. 27 | request(params, function(err, resp, data) { 28 | // Use err.status of 400 as it's not an unexpected application error, 29 | // but likely due to a bad request. Catches ECONNREFUSED, 30 | // getaddrinfo ENOENT, etc. 31 | if (err || !data || !resp || resp.statusCode !== 200) { 32 | var reqErr = new Error('Unable to load marker from URL.'); 33 | reqErr.originalError = err; 34 | return callback(reqErr); 35 | } 36 | // request 2.2.x *always* returns the response body as a string. 37 | // @TODO remove this once request is upgraded. 38 | if (!(data instanceof Buffer)) data = new Buffer(data, 'binary'); 39 | // Restrict data length. 40 | if (data.length > 32768) return callback(new Error('Marker loaded from URL is too large.')); 41 | callback(null, data); 42 | }); 43 | }; 44 | 45 | function normalizeParams(options) { 46 | var params = {}; 47 | 48 | if(typeof options === 'string') { 49 | params.uri = options; 50 | } else if(typeof options === 'object') { 51 | params = xtend(params, options); 52 | } else { 53 | return false; 54 | } 55 | 56 | var uri = params.uri || params.url; 57 | if (typeof params.agent === 'undefined') { 58 | // Don't use keepalive agent for https. 59 | params.agent = uri.indexOf('https') === 0 ? null : agent; 60 | } 61 | 62 | return params; 63 | } 64 | 65 | // allows passing in a custom request handler 66 | function getClient() { 67 | if (module.exports.requestClient) { return module.exports.requestClient; } 68 | return requestLib.defaults({ 69 | timeout: 5000, 70 | encoding: 'binary', 71 | headers: { 72 | 'accept-encoding': 'binary' 73 | } 74 | }); 75 | } 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/mapbox/geojson-mapnikify.svg)](https://travis-ci.org/mapbox/geojson-mapnikify) [![Coverage Status](https://coveralls.io/repos/mapbox/geojson-mapnikify/badge.png)](https://coveralls.io/r/mapbox/geojson-mapnikify) [![Code Climate](https://codeclimate.com/github/mapbox/geojson-mapnikify/badges/gpa.svg)](https://codeclimate.com/github/mapbox/geojson-mapnikify) 2 | 3 | # geojson-mapnikify 4 | 5 | Transform [GeoJSON](http://geojson.org/) objects into [Mapnik](http://mapnik.org/) 6 | XML stylesheets with embedded GeoJSON data and [simplestyle-spec](https://github.com/mapbox/simplestyle-spec)-derived 7 | styles. 8 | 9 | ## install 10 | 11 | As a dependency: 12 | 13 | npm install --save @kartotherian/geojson-mapnikify 14 | 15 | As a binary: 16 | 17 | npm install -g @kartotherian/geojson-mapnikify 18 | 19 | ## api 20 | 21 | Assumptions: 22 | 23 | * GeoJSON is valid, and in EPSG:4326 24 | * Styles, if any, are expressed in simplestyle-spec 25 | * Mapnik **3.x** is the rendering engine 26 | 27 | ## binary 28 | 29 | If you install `-g`, you can use `geojson-mapnikify` as a binary that takes 30 | a single GeoJSON file as an argument and writes a Mapnik XML stylesheet 31 | to stdout. 32 | 33 | ``` 34 | $ geojson-mapnikify test/data/point-retina.geojson > stylesheet.xml 35 | $ geojson-mapnikify test/data/point-retina.geojson retina > stylesheet-retina.xml 36 | ``` 37 | 38 | ### `mapnikify(geojson, retina, callback)` 39 | 40 | Transform GeoJSON into Mapnik XML. 41 | 42 | * `geojson` is a GeoJSON object. 43 | * `retina` is true or false for whether the style should be optimized for 2x rendering. 44 | * `callback` called with `(err, xml)` where xml is a string 45 | 46 | ### url markers 47 | 48 | If your GeoJSON object has one or more features with a `marker-url` property, `mapnikify()` will write the images found at the url into a file in a temporary directory and use that path in the Mapnik XML. This uses the [request library](https://www.npmjs.com/package/request) to handle the http file fetching. 49 | 50 | By default the request will attempt to fetch binary data from the specified url. If the url is `http` and not `https` , Mapnikify will use [agentkeepalive](https://www.npmjs.com/package/agentkeepalive) to speed up requesting multiple images. There is also a default timeout of 5 seconds. 51 | 52 | You can customize the defaults passed to `request()` . Simply set a custom wrapper defined with `request.defaults` . See [request's documentation on defaults](https://www.npmjs.com/package/request#requestdefaultsoptions) for more information. For a quick example, this will set a longer timeout: 53 | 54 | ```javascript 55 | var mapnikify = require('mapnikify'); 56 | var myRequest = require('request').defaults({ 57 | timeout: 10000, 58 | followRedirect: false 59 | }); 60 | mapnikify.setRequestClient(myRequest); 61 | 62 | mapnikify(geojson, retina, callback); 63 | 64 | mapnikify.setRequestClient(null); // return to mapnikify defaults 65 | ``` 66 | -------------------------------------------------------------------------------- /test/defaults.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'), 2 | fs = require('fs'), 3 | defaults = require('../lib/defaults.js'); 4 | 5 | test('defaults', function(t) { 6 | t.deepEqual(defaults({ 7 | type: 'Feature', 8 | geometry: { 9 | type: 'Point', 10 | coordinates: [0, 0] 11 | }, 12 | properties: {} 13 | }), 14 | { 15 | type: 'Feature', 16 | geometry: { 17 | type: 'Point', 18 | coordinates: [0, 0] 19 | }, 20 | properties: { 21 | "marker-color" : "7e7e7e", 22 | "marker-size" : "medium", 23 | "symbol" : "-" 24 | } 25 | } 26 | , 'point'); 27 | 28 | 29 | t.deepEqual(defaults({ 30 | type: 'Feature', 31 | geometry: { 32 | type: 'Point', 33 | coordinates: [0, 0] 34 | }, 35 | properties: { 36 | 'marker-color': '#f00' 37 | } 38 | }), 39 | { 40 | type: 'Feature', 41 | geometry: { 42 | type: 'Point', 43 | coordinates: [0, 0] 44 | }, 45 | properties: { 46 | "marker-color" : "#f00", 47 | "marker-size" : "medium", 48 | "symbol" : "-" 49 | } 50 | } 51 | , 'point'); 52 | 53 | t.deepEqual(defaults({ 54 | type: 'Feature', 55 | geometry: { 56 | type: 'Point', 57 | coordinates: [0, 0] 58 | }, 59 | properties: { 60 | 'marker-size': 'small' 61 | } 62 | }), 63 | { 64 | type: 'Feature', 65 | geometry: { 66 | type: 'Point', 67 | coordinates: [0, 0] 68 | }, 69 | properties: { 70 | "marker-color" : "7e7e7e", 71 | "marker-size" : "small", 72 | "symbol" : "-" 73 | } 74 | } 75 | , 'point'); 76 | 77 | t.deepEqual(defaults({ 78 | type: 'Feature', 79 | geometry: { 80 | type: 'LineString', 81 | coordinates: [[0, 0], [20, 20]] 82 | }, 83 | properties: { 84 | } 85 | }), 86 | { 87 | type: 'Feature', 88 | geometry: { 89 | type: 'LineString', 90 | coordinates: [[0, 0], [20, 20]] 91 | }, 92 | properties: { 93 | "stroke" : "#555555", // != undefined 94 | "stroke-width" : 2, // != undefined 95 | "stroke-opacity" : 1 // != undefined 96 | } 97 | } 98 | , 'linestring'); 99 | 100 | t.deepEqual(defaults({ 101 | type: 'Feature', 102 | geometry: { 103 | type: 'Polygon', 104 | coordinates: [[[0, 0], [20, 20]]] 105 | }, 106 | properties: { 107 | } 108 | }), 109 | { 110 | type: 'Feature', 111 | geometry: { 112 | type: 'Polygon', 113 | coordinates: [[[0, 0], [20, 20]]] 114 | }, 115 | properties: { 116 | "fill" : "#555555", // != undefined 117 | "fill-opacity" : 0.6, // != undefined 118 | "stroke" : "#555555", // != undefined 119 | "stroke-width" : 2, // != undefined 120 | "stroke-opacity" : 1 // != undefined 121 | } 122 | }, 'polygon'); 123 | 124 | t.end(); 125 | }); 126 | -------------------------------------------------------------------------------- /test/normalizestyle.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | var normalizeStyle = require('../lib/normalizestyle.js'); 3 | 4 | test('normalizeStyle point', function(t) { 5 | function checkPoint(properties, expected, message) { 6 | t.deepEqual(normalizeStyle({ 7 | type: 'Feature', 8 | geometry: { type: 'Point', coordinates: [0, 0] }, 9 | properties: properties, 10 | }).properties, expected, message); 11 | } 12 | checkPoint({ 'marker-size': 'HUGE' }, {}, 'point: marker-size HUGE => undefined'); 13 | checkPoint({ 'marker-size': 'small' }, { 'marker-size': 'small' }, 'point: marker-size small => small'); 14 | checkPoint({ 'marker-color': 'red' }, {}, 'point: marker-color red => undefined'); 15 | checkPoint({ 'marker-color': 'ffff' }, {}, 'point: marker-color ffff => undefined'); 16 | checkPoint({ 'marker-color': '' }, {}, 'point: marker-color "" => undefined'); 17 | checkPoint({ 'marker-color': '#ff0000' }, { 'marker-color': '#ff0000' }, 'point: marker-color #ff0000 => #ff0000'); 18 | checkPoint({ 'marker-color': 'ff0000' }, { 'marker-color': '#ff0000' }, 'point: marker-color ff0000 => #ff0000'); 19 | checkPoint({ 'marker-color': '#f00' }, { 'marker-color': '#f00' }, 'point: marker-color #f00 => #f00'); 20 | checkPoint({ 'marker-color': 'f00' }, { 'marker-color': '#f00' }, 'point: marker-color f00 => #f00'); 21 | t.end(); 22 | }); 23 | 24 | test('normalizeStyle fill', function(t) { 25 | function checkFill(properties, expected, message) { 26 | t.deepEqual(normalizeStyle({ 27 | type: 'Feature', 28 | geometry: { type: 'LineString', coordinates: [[0, 0]] }, 29 | properties: properties, 30 | }).properties, expected, message); 31 | } 32 | 33 | checkFill({ 'stroke': 'red' }, {}, 'fill: stroke red => undefined'); 34 | checkFill({ 'stroke': 'ffff' }, {}, 'fill: stroke ffff => undefined'); 35 | checkFill({ 'stroke': '' }, {}, 'fill: stroke "" => undefined'); 36 | checkFill({ 'stroke': '#ff0000' }, { 'stroke': '#ff0000' }, 'fill: stroke #ff0000 => #ff0000'); 37 | checkFill({ 'stroke': 'ff0000' }, { 'stroke': '#ff0000' }, 'fill: stroke ff0000 => #ff0000'); 38 | checkFill({ 'stroke': '#f00' }, { 'stroke': '#f00' }, 'fill: stroke #f00 => #f00'); 39 | checkFill({ 'stroke': 'f00' }, { 'stroke': '#f00' }, 'fill: stroke f00 => #f00'); 40 | checkFill({ 'stroke-width': 'red' }, {}, 'fill: stroke-width red => undefined'); 41 | checkFill({ 'stroke-width': 1.5 }, { 'stroke-width': 1.5 }, 'fill: stroke-width 1.5 => 1.5'); 42 | checkFill({ 'stroke-width': '1.5' }, { 'stroke-width': 1.5 }, 'fill: stroke-width "1.5" => 1.5'); 43 | checkFill({ 'stroke-opacity': 'red' }, {}, 'fill: stroke-opacity red => undefined'); 44 | checkFill({ 'stroke-opacity': 0.5 }, { 'stroke-opacity': 0.5 }, 'fill: stroke-opacity 0.5 => 0.5'); 45 | checkFill({ 'stroke-opacity': '0.5' }, { 'stroke-opacity': 0.5 }, 'fill: stroke-opacity "0.5" => 0.5'); 46 | 47 | checkFill({ 'fill': 'red' }, {}, 'fill: fill red => undefined'); 48 | checkFill({ 'fill': 'ffff' }, {}, 'fill: fill ffff => undefined'); 49 | checkFill({ 'fill': '' }, {}, 'fill: fill "" => undefined'); 50 | checkFill({ 'fill': '#ff0000' }, { 'fill': '#ff0000' }, 'fill: fill #ff0000 => #ff0000'); 51 | checkFill({ 'fill': 'ff0000' }, { 'fill': '#ff0000' }, 'fill: fill ff0000 => #ff0000'); 52 | checkFill({ 'fill': '#f00' }, { 'fill': '#f00' }, 'fill: fill #f00 => #f00'); 53 | checkFill({ 'fill': 'f00' }, { 'fill': '#f00' }, 'fill: fill f00 => #f00'); 54 | checkFill({ 'fill-opacity': 'red' }, {}, 'fill: fill-opacity red => undefined'); 55 | checkFill({ 'fill-opacity': 0.5 }, { 'fill-opacity': 0.5 }, 'fill: fill-opacity 0.5 => 0.5'); 56 | checkFill({ 'fill-opacity': '0.5' }, { 'fill-opacity': 0.5 }, 'fill: fill-opacity "0.5" => 0.5'); 57 | 58 | 59 | t.end(); 60 | }); 61 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var normalize = require('@mapbox/geojson-normalize'), 2 | makizushi = require('@kartotherian/makizushi'), 3 | queue = require('queue-async'), 4 | path = require('path'), 5 | fs = require('fs'), 6 | sigmund = require('sigmund'), 7 | enforceDefaults = require('./lib/defaults.js'), 8 | normalizeStyle = require('./lib/normalizestyle.js'), 9 | cachepath = require('./lib/cachepath.js'), 10 | loadURL = require('./lib/urlmarker.js'), 11 | get = require('./lib/get.js'); 12 | 13 | var template = fs.readFileSync(__dirname + '/lib/template.xml', 'utf8'); 14 | 15 | module.exports = generateXML; 16 | 17 | function generateXML(data, retina, callback) { 18 | var gj = normalize(data), 19 | q = queue(1); 20 | 21 | if (!gj) return callback(new Error('invalid GeoJSON')); 22 | 23 | for (var i = 0; i < gj.features.length; i++) { 24 | gj.features[i] = !markerURL(gj.features[i]) ? enforceDefaults(normalizeStyle(gj.features[i])) : normalizeStyle(gj.features[i]); 25 | } 26 | 27 | gj.features.filter(isPoint).forEach(function(feat, i) { 28 | if (markerURL(feat)) { 29 | q.defer(getRemote, feat, retina); 30 | } else { 31 | q.defer(getMarker, feat, retina); 32 | } 33 | }); 34 | 35 | q.awaitAll(done); 36 | 37 | function done(err, ls) { 38 | if (err) return callback(err); 39 | return callback(null, 40 | template.replace('{{geojson}}', JSON.stringify(gj))); 41 | } 42 | } 43 | 44 | function getRemote(feature, retina, callback) { 45 | var path = cachepath(markerURL(feature) + feature.properties['marker-color']) + '.png'; 46 | 47 | var written = function(err) { 48 | if (err) return callback(err); 49 | feature.properties['marker-path'] = path; 50 | callback(null, path); 51 | }; 52 | 53 | fs.exists(path, function(exists) { 54 | if (exists) { 55 | return written(null); 56 | } else { 57 | loadURL(feature, function urlLoaded(err, data) { 58 | if (err) return callback(err); 59 | fs.writeFile(path, data, written); 60 | }); 61 | } 62 | }); 63 | } 64 | 65 | function getMarker(feature, retina, callback) { 66 | var fp = feature.properties || {}, 67 | size = fp['marker-size'][0], 68 | symbol = fp['marker-symbol'] ? fp['marker-symbol'] : '', 69 | color = fp['marker-color'].replace('#', ''); 70 | 71 | var options = { 72 | tint: color, 73 | base: 'pin', 74 | symbol: symbol, 75 | retina: retina, 76 | size: size 77 | }; 78 | 79 | var path = cachepath(JSON.stringify(options)) + '.png'; 80 | 81 | fs.exists(path, function(exists) { 82 | if (exists) { 83 | feature.properties['marker-path'] = path; 84 | return callback(null, path); 85 | } else { 86 | makizushi(options, rendered); 87 | } 88 | }); 89 | 90 | function rendered(err, data) { 91 | if (err) return callback(err); 92 | fs.writeFile(path, data, written); 93 | } 94 | 95 | function written(err) { 96 | if (err) { 97 | return callback(err); 98 | } else { 99 | feature.properties['marker-path'] = path; 100 | callback(null, path); 101 | } 102 | } 103 | } 104 | 105 | function isPoint(feature) { 106 | return feature.geometry && 107 | (feature.geometry.type === 'Point' || 108 | feature.geometry.type === 'MultiPoint'); 109 | } 110 | 111 | function markerURL(feature) { 112 | return (feature.properties || {})['marker-url']; 113 | } 114 | 115 | function setRequestClient(requestClient){ 116 | get.requestClient = requestClient; 117 | } 118 | 119 | module.exports.setRequestClient = setRequestClient; 120 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'), 2 | fs = require('fs'), 3 | os = require('os'), 4 | path = require('path'), 5 | glob = require('glob'), 6 | nock = require('nock'), 7 | mapnik = require('@kartotherian/mapnik'), 8 | cachepath = require('../lib/cachepath.js'), 9 | urlmarker = require('../lib/urlmarker.js'), 10 | request = require('request'), 11 | generatexml = require('../'); 12 | 13 | mapnik.register_default_input_plugins(); 14 | 15 | function normalize(_) { 16 | return _.replace(/"marker-path":"([^\"])+"/g, '"marker-path":"TMP"'); 17 | } 18 | 19 | function render(xml, cb) { 20 | var map = new mapnik.Map(600, 400); 21 | var im = new mapnik.Image(map.width, map.height); 22 | 23 | map.fromString(xml, { 24 | strict: true 25 | }, function(err, map) { 26 | if (err) return cb(err); 27 | try { 28 | map.zoomAll(); 29 | var e = map.extent; 30 | // inflate bbox slightly in order to show single points 31 | var pad = 1; 32 | map.extent = [e[0] - pad, e[1] - pad, e[2] + pad, e[3] + pad]; 33 | map.render(im, {}, cb); 34 | } catch (err) { 35 | return cb(err); 36 | } 37 | }); 38 | } 39 | 40 | function generates(t, retina, name, message) { 41 | t.test(name, function(t) { 42 | var file_path = __dirname + '/data/' + name + '.geojson'; 43 | generatexml(JSON.parse(fs.readFileSync(file_path)), retina, function(err, xml) { 44 | if (message !== undefined) { 45 | t.equal(err.message, message, name + ' error returned'); 46 | t.end(); 47 | return; 48 | } 49 | t.equal(err, null, name + ' no error returned'); 50 | t.pass('is generated'); 51 | if (process.env.UPDATE) { 52 | fs.writeFileSync(__dirname + '/data/' + name + '.xml', normalize(xml)); 53 | } 54 | t.equal( 55 | normalize(xml), 56 | normalize(fs.readFileSync(__dirname + '/data/' + name + '.xml', 'utf8')), name); 57 | 58 | render(xml, function(err, im) { 59 | var expected_image = file_path + '.png'; 60 | t.equal(err, null); 61 | t.ok(im, 'creates image'); 62 | if (process.env.UPDATE) { 63 | fs.writeFileSync(__dirname + '/data/' + name + '.xml', xml); 64 | im.save(expected_image, 'png32'); 65 | } 66 | t.equal(0, im.compare(new mapnik.Image.open(expected_image))); 67 | t.end(); 68 | }); 69 | }); 70 | }); 71 | } 72 | 73 | test('clean tmp', function(t) { 74 | glob.sync(os.tmpdir() + 'geojson-mapnikify*').forEach(function(f) { 75 | fs.unlinkSync(f); 76 | }); 77 | t.end(); 78 | }); 79 | 80 | test('generatexml', generateXML); 81 | 82 | test('generatexml - cached', generateXML); 83 | 84 | function generateXML(t) { 85 | generates(t, false, 'example'); 86 | generates(t, false, 'point'); 87 | generates(t, false, 'multipoint'); 88 | generates(t, true, 'point-retina'); 89 | generates(t, true, 'dedup'); 90 | generates(t, true, 'point-retina'); 91 | generates(t, true, 'example-retina'); 92 | generates(t, true, 'feature-nullproperties'); 93 | generates(t, false, 'stroked'); 94 | generates(t, false, 'hashless'); 95 | t.end(); 96 | } 97 | 98 | test('corners', function(t) { 99 | generatexml(null, false, function(err, xml) { 100 | t.deepEqual(err, new Error('invalid GeoJSON')); 101 | t.end(); 102 | }); 103 | }); 104 | 105 | test('cachepath', function(t) { 106 | t.equal(typeof cachepath('foo'), 'string'); 107 | t.end(); 108 | }); 109 | 110 | test('urlmarker-too-large', function(t) { 111 | var file = fs.readFileSync(path.resolve(__dirname, 'data', 'too-large.png')); 112 | var scope = nock('http://devnull.mapnik.org') 113 | .get(/.*/) 114 | .reply(200, file, { 'content-type': 'image/png' }); 115 | 116 | urlmarker({ 117 | properties: { 118 | 'marker-url': 'http://devnull.mapnik.org/too-large.png' 119 | } 120 | }, function(err, res) { 121 | t.equal(err.message, 'Marker image size must not exceed 160000 pixels.'); 122 | nock.cleanAll(); 123 | t.end(); 124 | }); 125 | }); 126 | 127 | test('urlmarker-jpg', function(t) { 128 | var file = fs.readFileSync(path.resolve(__dirname, 'data', 'mountain.jpg')); 129 | var scope = nock('https://devnull.mapnik.org') 130 | .get(/.*/) 131 | .reply(200, file, { 'content-type': 'image/jpg' }); 132 | 133 | urlmarker({ 134 | properties: { 135 | 'marker-url': 'https://devnull.mapnik.org/mountain.jpg' 136 | } 137 | }, function(err, res) { 138 | t.equal(err.message, 'Marker image format is not supported.'); 139 | nock.cleanAll() 140 | t.end(); 141 | }); 142 | }); 143 | 144 | test('urlmarker-custom-client', function(t) { 145 | var client = request.defaults({ timeout: 100, encoding: 'binary' }); 146 | var file = fs.readFileSync(path.resolve(__dirname, 'data', 'rocket.png')); 147 | var scope = nock('http://devnull.mapnik.org') 148 | .get(/.*/) 149 | .delay(300) 150 | .reply(200, file, { 'content-type': 'image/png' }); 151 | 152 | generatexml.setRequestClient(client); 153 | urlmarker({ 154 | properties: { 155 | 'marker-url': 'http://devnull.mapnik.org/rocket.png' 156 | } 157 | }, function(err, res) { 158 | t.equal(err.message, 'Unable to load marker from URL.'); 159 | t.equal(err.originalError.code, 'ESOCKETTIMEDOUT'); 160 | generatexml.setRequestClient(null); 161 | nock.cleanAll(); 162 | t.end(); 163 | }); 164 | }); 165 | 166 | test('urlmarker-too-large-custom-client', function(t) { 167 | var client = request.defaults({ timeout: 100, encoding: 'binary' }); 168 | var file = fs.readFileSync(path.resolve(__dirname, 'data', 'html-page.png')); 169 | var scope = nock('http://devnull.mapnik.org') 170 | .get(/.*/) 171 | .reply(200, file, { 'content-type': 'text/html' }); 172 | 173 | generatexml.setRequestClient(client); 174 | urlmarker({ 175 | properties: { 176 | 'marker-url': 'http://devnull.mapnik.org/html-page.png' 177 | } 178 | }, function(err, res) { 179 | t.equal(err.message, 'Marker loaded from URL is too large.'); 180 | generatexml.setRequestClient(null); 181 | nock.cleanAll(); 182 | t.end(); 183 | }); 184 | }); 185 | 186 | // ensure generatexml.setRequestClient(null) returns to default client 187 | test('urlmarker-uncustomize-client', function(t) { 188 | var file = fs.readFileSync(path.resolve(__dirname, 'data', 'rocket.png')); 189 | var scope = nock('http://devnull.mapnik.org', { reqheaders: {'accept-encoding': 'binary'}}) 190 | .get(/.*/) 191 | .delay(300) 192 | .reply(200, file, { 'content-type': 'image/png' }); 193 | 194 | urlmarker({ 195 | properties: { 196 | 'marker-url': 'http://devnull.mapnik.org/rocket.png' 197 | } 198 | }, function(err, res) { 199 | t.ifErr(err); 200 | nock.cleanAll(); 201 | t.end(); 202 | }); 203 | }); 204 | -------------------------------------------------------------------------------- /test/data/html-page.png: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | PC | DLC Management 9 | 10 | 53 | 54 | 63 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 86 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 121 |
122 |
123 | 124 |
125 | 128 | 131 | 132 |
133 | 134 | 165 | 182 | 183 |
187 |
188 | 189 | 244 | 248 | 249 | 250 | 251 |
677 | 678 |
679 |
680 |
681 |
682 |
683 |
684 |
685 |
686 |
687 |
688 |

Test Property Josh

689 |
690 |
691 |
692 |
693 |
694 | 695 |
696 |
697 |
698 |
699 |
700 | 701 |
702 | 703 |
704 |

Demographics

705 |
706 | 707 |
708 | 709 | 710 |
711 |
712 |
713 |
714 |

Documents

715 |
716 |
717 |
718 |
719 |
720 | 721 |
722 |

Property Highlights

723 | 724 |

725 |
311 4th ave
san diego, CA 92101
726 |

727 |
    728 |
  • 729 | County: San Diego
  • 730 |
  • 731 | Type: Community
  • 732 |
733 | 734 | 735 | Tenancy Applications 736 | 737 |
738 | 739 | 740 |
741 |

April Tschappat

742 | 743 |

744 | 745 | Email April 746 | 747 |

748 |
    749 |
  • 750 | Phone: 972.678.4939
  • 751 |
  • 752 | Cell: 847.909.8143
  • 753 |
754 | 755 |
756 | 757 |
758 |
759 |
760 |
761 | 762 |
763 |
764 |
765 |
766 | 767 |
768 | 779 |
780 | 781 | 782 |
783 | 792 | 793 | 794 | 795 | 796 | 797 | 798 |
799 |
800 | 801 |
802 |
803 | 812 |
813 |
814 |
815 | 820 |
821 |
822 |
823 | 824 | 825 | 828 | 831 | 834 | 837 | 838 |
826 |
827 |
829 |
Available
830 |
832 |
833 |
835 |
Coming Available
836 |
839 |
840 |
841 | 842 | 843 |
844 | 845 |
846 |
847 | 848 |
849 |
850 | 851 |
852 |
853 | 854 |
855 | 856 |
857 | current tenants 858 |
859 | 860 |
861 | 862 |
863 |
864 | 865 |
866 |
867 | 868 |
869 |
870 |
871 | 872 |
873 | 874 |
875 |
876 |
877 |
878 | Options 879 |
880 |
881 |
882 |
883 |
884 | 889 |
890 | 891 |
892 | 898 |
899 | 900 |
901 | 906 |
907 | 908 |
909 | 914 |
915 |
916 |
917 |
918 | 919 | 920 |
921 | 922 |
923 |
924 |
925 |
926 |
927 | 928 |
929 | 930 |
931 |
932 |
933 | Great Places to Work 934 | 937 |
938 | 939 |
940 |
941 |
942 |
943 |
944 |
950 | 951 |
952 |
959 |
960 | 961 |
962 | 963 | 964 |
965 | 966 | Great Places to Work 967 | 968 |
969 | 970 | 971 |
972 | 973 |
974 | 975 | 976 |
977 | 980 |
981 | 982 | 983 | 984 |
985 | 1002 |
1003 |
1004 |
1005 | 1006 | 1007 |
1008 | 1011 |
1012 | 1013 | 1014 |
1015 |
1016 | 1020 | 1021 | 1022 | 1122 | 1123 | 1126 | 1127 | 1128 | 1129 | 1133 | 1134 | 1135 | 1136 | 1137 | --------------------------------------------------------------------------------