├── .gitignore ├── package.json ├── README.md ├── test ├── index.html └── orbitControls.js ├── src ├── loader.js ├── main.js ├── builder.js ├── parser.js └── importer.js └── bin └── osmthree.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | app.js 3 | test/fake_osm.json 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "osmthree", 3 | "version": "0.0.1", 4 | "description": "Import OSM data into a three.js scene", 5 | "main": "bin/osmthree.js", 6 | "bin": { 7 | "osmthree": "osmthree.js" 8 | }, 9 | "directories": { 10 | "test": "test" 11 | }, 12 | "scripts": { 13 | "test": "echo \"Error: no test specified\" && exit 1" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git@github.com:skewart/osmthree.git" 18 | }, 19 | "keywords": [ 20 | "OSM", 21 | "three.js", 22 | "WebGL", 23 | "buildings" 24 | ], 25 | "author": "Scott Ewart", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/skewart/osmthree/issues" 29 | }, 30 | "homepage": "https://github.com/skewart/osmthree" 31 | } 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | OSMThree 2 | ======== 3 | 4 | OSMThree is a library that adds 3d models of buildings to a three.js scene. It fetches data describing the buildings 5 | from the Open Street Maps database via the Overpass API, builds one or more THREE.Mesh objects for the buildings, and 6 | adds them to the scene. 7 | 8 | OSMThree is still in the early stages of development, so it may be a little rough around the edges. 9 | 10 | ### How to get it 11 | 12 | Either clone the github repo and use one of the files in the build directory, or grab it via NPM. 13 | 14 | ### How to use it 15 | 16 | Currently there is only one public function in the library, makeBuildings. It takes a THREE.Scene object, a bounding box 17 | array, and an options object as its arguments. It executes asynchronously and returns nothing. 18 | 19 | Suppose the variable myScene points to a THREE.Scene object... 20 | 21 | ``` 22 | OSM3.makeBuildings( myScene, [ 114.15, 22.2675, 114.165, 22.275 ] ) 23 | ``` 24 | 25 | This would add all the buildings found in the Open Street Maps database within the given bounding box (which happens to be central Hong Kong) 26 | to myScene using all of the default options. 27 | 28 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/loader.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | // Loader is responsible for fetching data from the Overpass API 4 | 5 | function constructor() { 6 | 7 | var OSM_XAPI_URL = 'http://overpass-api.de/api/interpreter?data=[out:json];(way[%22building%22]({s},{w},{n},{e});node(w);way[%22building:part%22=%22yes%22]({s},{w},{n},{e});node(w);relation[%22building%22]({s},{w},{n},{e});way(r);node(w););out;'; 8 | 9 | var req = new XMLHttpRequest(); 10 | 11 | function xhr(url, param, callback) { 12 | 13 | url = url.replace(/\{ *([\w_]+) *\}/g, function(tag, key) { 14 | return param[key] || tag; 15 | }); 16 | 17 | req.onerror = function() { 18 | req.status = 500; 19 | req.statusText = 'Error'; 20 | }; 21 | 22 | req.ontimeout = function() { 23 | req.status = 408; 24 | req.statusText = 'Timeout'; 25 | }; 26 | 27 | req.onprogress = function() { 28 | }; 29 | 30 | req.onload = function() { 31 | req.status = 200; 32 | req.statusText = 'Ok'; 33 | }; 34 | 35 | req.onreadystatechange = function() { 36 | if (req.readyState !== 4) { 37 | return; 38 | } 39 | if (!req.status || req.status < 200 || req.status > 299) { 40 | return; 41 | } 42 | if (callback && req.responseText) { 43 | callback( JSON.parse(req.responseText) ); 44 | } 45 | } 46 | 47 | req.open('GET', url); 48 | req.send(null); 49 | 50 | }; 51 | 52 | 53 | // load fetches data from the Overpass API for the given bounding box 54 | // PARAMETERS: 55 | // bbox --> a four float array consisting of [ , , , ], 56 | // callback --> a callback function to be called when the data is returned 57 | this.load = function( bbox, callback ) { 58 | var params = { 59 | e: bbox[2], 60 | n: bbox[3], 61 | s: bbox[1], 62 | w: bbox[0] 63 | } 64 | xhr( OSM_XAPI_URL, params, callback ); 65 | } 66 | 67 | } 68 | 69 | module.exports = constructor; -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | 2 | var Loader = require("./loader.js"), 3 | Parser = require("./parser.js"), 4 | Builder = require("./builder.js"), 5 | ngeo = require('ngeohash'); // TEMPORARY! Or maybe not? 6 | 7 | // makeBuildings fetches data from the Overpass API and builds three.js 3d models of buildings for everything 8 | // found within the given bounding box. 9 | // PARAMETERS: 10 | // callback --> a function that gets called when a building mesh is completely built and ready to be added to a scene 11 | // bbox --> a four float array specifying the min and max latitude and longitude coordinates whithin which to fetch 12 | // buildings. [ , , , ] (Note: It's lon,lat not lat,lon) 13 | // params --> an object that contains optional parameters to further control how the buildings are created. See the source code. 14 | function makeBuildings( callback, bbox, params ) { 15 | 16 | var 17 | buildOpts = {}, 18 | params = params || {}, 19 | origin = params.origin || [ bbox[0], bbox[1] ], // an array, [ lon, lat ], describing the poisition of the scene's origin 20 | units = params.units || 'meter', // 'meter', 'foot', 'inch', or 'millimeter' 21 | scale = params.scale || 1.0, // float describing how much to scale the units for the scene 22 | onDataReady = params.onDataReady || false; // called when data is loaded from Overpass, before THREE objects are created 23 | 24 | buildOpts.mergeGeometry = params.mergeGeometry || false; // create one big Geometry and Mesh with all the buildings 25 | buildOpts.defaultColor = params.defaultColor || false; // most buildings will be this color - default is 0xF0F0F0 26 | buildOpts.meshFunction = params.meshFunction || false; // custom function for creating the THREE.Mesh objects 27 | 28 | var 29 | builder = new Builder( callback, scale, origin, buildOpts ), 30 | parser = new Parser( builder.build, onDataReady ), 31 | loader = new Loader(); 32 | 33 | loader.load( bbox, parser.parse ); 34 | 35 | } 36 | 37 | 38 | // Just gets the building data from the Overpass API and calls the callback, passing in the building data. 39 | function fetchBldgData( callback, bbox, params ) { 40 | 41 | var onDataReady = params.onDataReady || false, 42 | parser = new Parser( callback, onDataReady ), 43 | loader = new Loader(); 44 | 45 | loader.load( bbox, parser.parse ); 46 | 47 | } 48 | 49 | 50 | // Given some building data, creates meshes and calls the callback when it's done 51 | function buildBldgs( callback, buildingData, params ) { 52 | 53 | var buildOpts = {}, 54 | scale = params.scale || 1.0, 55 | origin = params.origin || findDefaultOrigin( buildingData ); 56 | 57 | buildOpts.mergeGeometry = params.mergeGeometry || false; 58 | buildOpts.defaultColor = params.defaultColor || false; 59 | buildOpts.meshFunction = params.meshFunction || false; 60 | 61 | var builder = new Builder( callback, scale, origin, buildOpts ); 62 | 63 | builder.build( buildingData ); 64 | 65 | } 66 | 67 | 68 | // 69 | function findDefaultOrigin( bldgs ) { 70 | console.log( bldgs ); 71 | return [ 0, 0 ]; 72 | } 73 | 74 | 75 | module.exports = { 76 | makeBuildings: makeBuildings, 77 | fetchBldgData: fetchBldgData, 78 | buildBldgs: buildBldgs 79 | } 80 | 81 | // Maybe put this in a separte wrapper file, included in a version for use in a non-NPM context 82 | window.OSM3 = { 83 | makeBuildings: makeBuildings, 84 | fetchBldgData: fetchBldgData, 85 | buildBldgs: buildBldgs 86 | } 87 | 88 | window.ngeo = ngeo // TEMPORARY!!!!! 89 | 90 | // TODO Go back to making the first argument to makeBuildings a callback instead of a THREE.Scene object. 91 | // Accept a THREE.Plane object as an optional argument, and then geohash from its XZ values (instead of lat-lon) to its Y values. 92 | // Export more fine-grained functions/modules within OSMthree that allow control over what happens and when, e.g. with Promises. 93 | // (should these maintain state? Probably not, they should accept arguments, I think. ) 94 | -------------------------------------------------------------------------------- /src/builder.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | function constructor( readyCallback, scale, origin, options ) { 4 | 5 | var 6 | options = options || {}, 7 | _readyCallback = readyCallback, 8 | _scale = scale, 9 | _origin = lonLatToWorld( origin[0], origin[1] ), 10 | _meshCallback = options.meshCallback || createMesh, 11 | _defaultColor = options.defaultColor || 0xf0f0f0; 12 | 13 | 14 | this.build = function( items ) { 15 | 16 | var bldg, currVerLen, 17 | mats = [], 18 | ids = []; 19 | //geom = new THREE.Geometry(); 20 | 21 | for ( var i=0, len=items.length; i < len; i++ ) { 22 | bldg = makeBldgGeom( items[i] ); 23 | if (bldg) { 24 | _readyCallback( _meshCallback.call( this, bldg, items[i] ) ); 25 | } 26 | //currVerLen = geom.vertices.length; 27 | //geom.vertices = geom.vertices.concat( bldg.vertices ); 28 | //geom.faces = geom.faces.concat( updateFaces( bldg.faces, currVerLen ) ); 29 | //mats = mats.concat( bldg.materials ); 30 | // Is this really necessary? 31 | //for ( var j = 0, fLen = bldg.faces.length; j < fLen; j++ ) { 32 | //ids.push( i ); 33 | //} 34 | } 35 | 36 | // TODO Create the mesh object and any necessary material objects 37 | //_scene.add( new THREE.Mesh( geom, new THREE.MeshNormalMaterial() ) ); 38 | 39 | } 40 | 41 | 42 | function updateFaces( faces, len ) { 43 | for ( var i=0, flen = faces.length; i < flen; i++ ) { 44 | faces[i].a += len; 45 | faces[i].b += len; 46 | faces[i].c += len; 47 | if ( faces[i].d ) { 48 | faces[i].d += len; 49 | } 50 | } 51 | return faces; 52 | } 53 | 54 | 55 | function createMesh( geom, osmData ) { 56 | // return new THREE.Mesh( geom, new THREE.MeshLambertMaterial() ); 57 | var face, 58 | mats = [], 59 | wci = 0, 60 | rci = 0; 61 | if ( osmData.wallColor ) { 62 | mats.push( new THREE.MeshLambertMaterial( {color: osmData.wallColor }) ); 63 | } else { 64 | mats.push( new THREE.MeshLambertMaterial( {color: _defaultColor } ) ); 65 | } 66 | if ( osmData.roofColor ) { 67 | mats.push( new THREE.MeshLambertMaterial( {color: osmData.roofColor }) ); 68 | rci = 1; 69 | } 70 | for ( var i=0, len=geom.faces.length; i < len; i++ ) { 71 | face = geom.faces[i]; 72 | ( face.normal.y === 1 ) ? face.materialIndex = rci 73 | : face.materialIndex = wci; 74 | } 75 | var m = new THREE.Mesh( geom, new THREE.MeshFaceMaterial( mats ) ); 76 | m.footprint = osmData.footprint; 77 | return m; 78 | } 79 | 80 | 81 | function lonLatToWorld( lon, lat ) { 82 | var x, y, pointX, pointY, latRad, mercN, 83 | worldWidth = 40075000, 84 | worldHeight = 40008000; 85 | 86 | x = ( lon + 180 ) * ( worldWidth / 360); 87 | latRad = lat*Math.PI/180; 88 | mercN = Math.log( Math.tan((Math.PI/4)+(latRad/2))); 89 | y = (worldHeight/2)-(worldHeight*mercN/(2*Math.PI)); 90 | return [ x, y ] 91 | } 92 | 93 | 94 | function lonLatToScene( lon, lat ) { 95 | var point = lonLatToWorld( lon, lat ); 96 | // This looks weird, and it is kind of a hack, but it's done because of the way THREE.ExtrudeGeometry converts 97 | // Vector2 x,y coordinates into x,z coordinates in Vector3 objects. +x +y goes to -z,-x. This effectively rotates 98 | // the geometries, putting them in the correct quadrant. Doing an actual rotation might be cleaner, but, well. 99 | return new THREE.Vector2( _origin[1] - point[1], _origin[0] - point[0] ); 100 | } 101 | 102 | 103 | function makeBldgGeom( item ) { 104 | // Create a path 105 | var pointX, pointY, extrudePath, eg, 106 | path, shapes, 107 | bldgHeight = item.height, 108 | pathPoints = []; 109 | 110 | for ( var i = 0, last = item.footprint.length-1; i < last; i+=2 ) { 111 | pathPoints.push( lonLatToScene( item.footprint[i+1], item.footprint[i] ) ); 112 | } 113 | 114 | path = new THREE.Path( pathPoints ); 115 | shapes = path.toShapes(); // isCCW, noHoles 116 | 117 | extrudePath = new THREE.CurvePath(); 118 | extrudePath.add( new THREE.LineCurve3( new THREE.Vector3(0,0,0), new THREE.Vector3(0,bldgHeight,0) ) ); 119 | 120 | eg = new THREE.ExtrudeGeometry( shapes, { 121 | extrudePath: extrudePath, 122 | material: 0 123 | }) 124 | 125 | return eg; 126 | 127 | } 128 | 129 | } 130 | 131 | module.exports = constructor; -------------------------------------------------------------------------------- /src/parser.js: -------------------------------------------------------------------------------- 1 | 2 | var importer = require('./importer.js' ); 3 | 4 | function constructor( finalCallback, filterCallback ) { 5 | 6 | var _nodes = {}, 7 | _ways = {}, 8 | _relations = {}, 9 | MAP_DATA = []; 10 | 11 | 12 | function isBuilding(data) { 13 | var tags = data.tags; 14 | return (tags && !tags.landuse && 15 | (tags.building || tags['building:part']) && (!tags.layer || tags.layer >= 0)); 16 | } 17 | 18 | 19 | function getRelationWays(members) { 20 | var m, outer, inner = []; 21 | for (var i = 0, il = members.length; i < il; i++) { 22 | m = members[i]; 23 | if (m.type !== 'way' || !_ways[m.ref]) { 24 | continue; 25 | } 26 | if (!m.role || m.role === 'outer') { 27 | outer = _ways[m.ref]; 28 | continue; 29 | } 30 | if (m.role === 'inner' || m.role === 'enclave') { 31 | inner.push(_ways[m.ref]); 32 | continue; 33 | } 34 | } 35 | 36 | // if (outer && outer.tags) { 37 | if (outer) { // allows tags to be attached to relation - instead of outer way 38 | return { outer:outer, inner:inner }; 39 | } 40 | } 41 | 42 | 43 | function getFootprint(points) { 44 | if (!points) { 45 | return; 46 | } 47 | 48 | var footprint = [], p; 49 | for (var i = 0, il = points.length; i < il; i++) { 50 | p = _nodes[ points[i] ]; 51 | footprint.push(p[0], p[1]); 52 | } 53 | 54 | // do not close polygon yet 55 | if (footprint[footprint.length-2] !== footprint[0] && footprint[footprint.length-1] !== footprint[1]) { 56 | footprint.push(footprint[0], footprint[1]); 57 | } 58 | 59 | // can't span a polygon with just 2 points (+ start & end) 60 | if (footprint.length < 8) { 61 | return; 62 | } 63 | 64 | return footprint; 65 | } 66 | 67 | 68 | 69 | function mergeItems(dst, src) { 70 | for (var p in src) { 71 | if (src.hasOwnProperty(p)) { 72 | dst[p] = src[p]; 73 | } 74 | } 75 | return dst; 76 | } 77 | 78 | 79 | function filterItem(item, footprint) { 80 | var res = importer.alignProperties(item.tags); 81 | res.tags = item.tags; // Keeping the raw tags too 82 | if (item.id) { 83 | res.id = item.id; 84 | } 85 | 86 | if (footprint) { 87 | res.footprint = importer.makeWinding(footprint, importer.clockwise); 88 | } 89 | 90 | if (res.shape === 'cone' || res.shape === 'cylinder') { 91 | res.radius = importer.getRadius(res.footprint); 92 | } 93 | 94 | return res; 95 | } 96 | 97 | 98 | function processNode(node) { 99 | _nodes[node.id] = [node.lat, node.lon]; 100 | } 101 | 102 | 103 | function processWay(way) { 104 | if (isBuilding(way)) { 105 | var item, footprint; 106 | if ( footprint = getFootprint(way.nodes) ) { 107 | item = filterItem(way, footprint); 108 | MAP_DATA.push(item); 109 | } 110 | return; 111 | } 112 | 113 | var tags = way.tags; 114 | if (!tags || (!tags.highway && !tags.railway && !tags.landuse)) { // TODO: add more filters 115 | _ways[way.id] = way; 116 | } 117 | } 118 | 119 | 120 | function processRelation(relation) { 121 | var relationWays, outerWay, holes = [], 122 | item, relItem, outerFootprint, innerFootprint; 123 | if (!isBuilding(relation) || 124 | (relation.tags.type !== 'multipolygon' && relation.tags.type !== 'building') ) { 125 | return; 126 | } 127 | 128 | if ((relationWays = getRelationWays(relation.members))) { 129 | relItem = filterItem(relation); 130 | if ((outerWay = relationWays.outer)) { 131 | if (outerFootprint = getFootprint(outerWay.nodes)) { 132 | item = filterItem(outerWay, outerFootprint); 133 | for (var i = 0, il = relationWays.inner.length; i < il; i++) { 134 | if ((innerFootprint = getFootprint(relationWays.inner[i].nodes))) { 135 | holes.push( importer.makeWinding(innerFootprint, importer.counterClockwise) ); 136 | } 137 | } 138 | if (holes.length) { 139 | item.holes = holes; 140 | } 141 | MAP_DATA.push( mergeItems(item, relItem) ); 142 | } 143 | } 144 | } 145 | } 146 | 147 | 148 | this.parse = function( osmData ) { 149 | var item, buildData; 150 | for ( var i = 0, len = osmData.elements.length; i < len; i++ ) { 151 | item = osmData.elements[i]; 152 | switch ( item.type ) { 153 | case 'node': processNode( item ); break; 154 | case 'way': processWay( item ); break; 155 | case 'relation': processRelation( item ); break; 156 | } 157 | } 158 | ( filterCallback ) ? buildData = filterCallback.call( this, MAP_DATA ) 159 | : buildData = MAP_DATA; 160 | finalCallback.apply( this, [ buildData ] ); 161 | } 162 | 163 | 164 | } 165 | 166 | module.exports = constructor; -------------------------------------------------------------------------------- /src/importer.js: -------------------------------------------------------------------------------- 1 | 2 | var YARD_TO_METER = 0.9144, 3 | FOOT_TO_METER = 0.3048, 4 | INCH_TO_METER = 0.0254, 5 | METERS_PER_LEVEL = 3, 6 | DEFAULT_HEIGHT = 5, 7 | 8 | clockwise = 'CW', 9 | counterClockwise = 'CCW'; 10 | 11 | 12 | module.exports = { 13 | 14 | YARD_TO_METER: YARD_TO_METER, 15 | FOOT_TO_METER: FOOT_TO_METER, 16 | INCH_TO_METER: INCH_TO_METER, 17 | METERS_PER_LEVEL: METERS_PER_LEVEL, 18 | DEFAULT_HEIGHT: DEFAULT_HEIGHT, 19 | 20 | clockwise: clockwise, 21 | counterClockwise: counterClockwise, 22 | 23 | // detect winding direction: clockwise or counter clockwise 24 | getWinding: function(points) { 25 | var x1, y1, x2, y2, 26 | a = 0, 27 | i, il; 28 | for (i = 0, il = points.length-3; i < il; i += 2) { 29 | x1 = points[i]; 30 | y1 = points[i+1]; 31 | x2 = points[i+2]; 32 | y2 = points[i+3]; 33 | a += x1*y2 - x2*y1; 34 | } 35 | return (a/2) > 0 ? this.clockwise : this.counterClockwise; 36 | }, 37 | 38 | // enforce a polygon winding direcetion. Needed for proper backface culling. 39 | makeWinding: function(points, direction) { 40 | var winding = this.getWinding(points); 41 | if (winding === direction) { 42 | return points; 43 | } 44 | var revPoints = []; 45 | for (var i = points.length-2; i >= 0; i -= 2) { 46 | revPoints.push(points[i], points[i+1]); 47 | } 48 | return revPoints; 49 | }, 50 | 51 | toMeters: function(str) { 52 | str = '' + str; 53 | var value = parseFloat(str); 54 | if (value === str) { 55 | return value <<0; 56 | } 57 | if (~str.indexOf('m')) { 58 | return value <<0; 59 | } 60 | if (~str.indexOf('yd')) { 61 | return value*this.YARD_TO_METER <<0; 62 | } 63 | if (~str.indexOf('ft')) { 64 | return value*this.FOOT_TO_METER <<0; 65 | } 66 | if (~str.indexOf('\'')) { 67 | var parts = str.split('\''); 68 | var res = parts[0]*this.FOOT_TO_METER + parts[1]*this.INCH_TO_METER; 69 | return res <<0; 70 | } 71 | return value <<0; 72 | }, 73 | 74 | getRadius: function(points) { 75 | var minLat = 90, maxLat = -90; 76 | for (var i = 0, il = points.length; i < il; i += 2) { 77 | minLat = min(minLat, points[i]); 78 | maxLat = max(maxLat, points[i]); 79 | } 80 | 81 | return (maxLat-minLat) / RAD * 6378137 / 2 <<0; // 6378137 = Earth radius 82 | }, 83 | 84 | materialColors: { 85 | brick:'#cc7755', 86 | bronze:'#ffeecc', 87 | canvas:'#fff8f0', 88 | concrete:'#999999', 89 | copper:'#a0e0d0', 90 | glass:'#e8f8f8', 91 | gold:'#ffcc00', 92 | plants:'#009933', 93 | metal:'#aaaaaa', 94 | panel:'#fff8f0', 95 | plaster:'#999999', 96 | roof_tiles:'#f08060', 97 | silver:'#cccccc', 98 | slate:'#666666', 99 | stone:'#996666', 100 | tar_paper:'#333333', 101 | wood:'#deb887' 102 | }, 103 | 104 | baseMaterials: { 105 | asphalt:'tar_paper', 106 | bitumen:'tar_paper', 107 | block:'stone', 108 | bricks:'brick', 109 | glas:'glass', 110 | glassfront:'glass', 111 | grass:'plants', 112 | masonry:'stone', 113 | granite:'stone', 114 | panels:'panel', 115 | paving_stones:'stone', 116 | plastered:'plaster', 117 | rooftiles:'roof_tiles', 118 | roofingfelt:'tar_paper', 119 | sandstone:'stone', 120 | sheet:'canvas', 121 | sheets:'canvas', 122 | shingle:'tar_paper', 123 | shingles:'tar_paper', 124 | slates:'slate', 125 | steel:'metal', 126 | tar:'tar_paper', 127 | tent:'canvas', 128 | thatch:'plants', 129 | tile:'roof_tiles', 130 | tiles:'roof_tiles' 131 | }, 132 | 133 | // cardboard 134 | // eternit 135 | // limestone 136 | // straw 137 | 138 | getMaterialColor: function(str) { 139 | str = str.toLowerCase(); 140 | if (str[0] === '#') { 141 | return str; 142 | } 143 | return this.materialColors[this.baseMaterials[str] || str] || null; 144 | }, 145 | 146 | // aligns and cleans up properties in place 147 | alignProperties: function(prop) { 148 | var item = {}; 149 | 150 | prop = prop || {}; 151 | 152 | item.height = this.toMeters(prop.height); 153 | if (!item.height) { 154 | if (prop['building:height']) { 155 | item.height = this.toMeters(prop['building:height']); 156 | } 157 | if (prop.levels) { 158 | item.height = prop.levels*this.METERS_PER_LEVEL <<0; 159 | } 160 | if (prop['building:levels']) { 161 | item.height = prop['building:levels']*this.METERS_PER_LEVEL <<0; 162 | } 163 | if (!item.height) { 164 | item.height = DEFAULT_HEIGHT; 165 | } 166 | } 167 | 168 | item.minHeight = this.toMeters(prop.min_height); 169 | if (!item.min_height) { 170 | if (prop['building:min_height']) { 171 | item.minHeight = this.toMeters(prop['building:min_height']); 172 | } 173 | if (prop.min_level) { 174 | item.minHeight = prop.min_level*this.METERS_PER_LEVEL <<0; 175 | } 176 | if (prop['building:min_level']) { 177 | item.minHeight = prop['building:min_level']*this.METERS_PER_LEVEL <<0; 178 | } 179 | } 180 | 181 | item.wallColor = prop.wallColor || prop.color; 182 | if (!item.wallColor) { 183 | if (prop.color) { 184 | item.wallColor = prop.color; 185 | } 186 | if (prop['building:material']) { 187 | item.wallColor = this.getMaterialColor(prop['building:material']); 188 | } 189 | if (prop['building:facade:material']) { 190 | item.wallColor = this.getMaterialColor(prop['building:facade:material']); 191 | } 192 | if (prop['building:cladding']) { 193 | item.wallColor = this.getMaterialColor(prop['building:cladding']); 194 | } 195 | // wall color 196 | if (prop['building:color']) { 197 | item.wallColor = prop['building:color']; 198 | } 199 | if (prop['building:colour']) { 200 | item.wallColor = prop['building:colour']; 201 | } 202 | } 203 | 204 | item.roofColor = prop.roofColor; 205 | if (!item.roofColor) { 206 | if (prop['roof:material']) { 207 | item.roofColor = this.getMaterialColor(prop['roof:material']); 208 | } 209 | if (prop['building:roof:material']) { 210 | item.roofColor = this.getMaterialColor(prop['building:roof:material']); 211 | } 212 | // roof color 213 | if (prop['roof:color']) { 214 | item.roofColor = prop['roof:color']; 215 | } 216 | if (prop['roof:colour']) { 217 | item.roofColor = prop['roof:colour']; 218 | } 219 | if (prop['building:roof:color']) { 220 | item.roofColor = prop['building:roof:color']; 221 | } 222 | if (prop['building:roof:colour']) { 223 | item.roofColor = prop['building:roof:colour']; 224 | } 225 | } 226 | 227 | switch (prop['building:shape']) { 228 | case 'cone': 229 | case 'cylinder': 230 | item.shape = prop['building:shape']; 231 | break; 232 | 233 | case 'dome': 234 | item.shape = 'dome'; 235 | break; 236 | 237 | case 'sphere': 238 | item.shape = 'cylinder'; 239 | break; 240 | } 241 | 242 | if ((prop['roof:shape'] === 'cone' || prop['roof:shape'] === 'dome') && prop['roof:height']) { 243 | item.shape = 'cylinder'; 244 | item.roofShape = prop['roof:shape']; 245 | item.roofHeight = this.toMeters(prop['roof:height']); 246 | } 247 | 248 | if (item.roofHeight) { 249 | item.height = max(0, item.height-item.roofHeight); 250 | } else { 251 | item.roofHeight = 0; 252 | } 253 | 254 | return item; 255 | } 256 | }; 257 | -------------------------------------------------------------------------------- /test/orbitControls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author qiao / https://github.com/qiao 3 | * @author mrdoob / http://mrdoob.com 4 | * @author alteredq / http://alteredqualia.com/ 5 | * @author WestLangley / http://github.com/WestLangley 6 | * @author erich666 / http://erichaines.com 7 | */ 8 | /*global THREE, console */ 9 | 10 | // This set of controls performs orbiting, dollying (zooming), and panning. It maintains 11 | // the "up" direction as +Y, unlike the TrackballControls. Touch on tablet and phones is 12 | // supported. 13 | // 14 | // Orbit - left mouse / touch: one finger move 15 | // Zoom - middle mouse, or mousewheel / touch: two finger spread or squish 16 | // Pan - right mouse, or arrow keys / touch: three finter swipe 17 | // 18 | // This is a drop-in replacement for (most) TrackballControls used in examples. 19 | // That is, include this js file and wherever you see: 20 | // controls = new THREE.TrackballControls( camera ); 21 | // controls.target.z = 150; 22 | // Simple substitute "OrbitControls" and the control should work as-is. 23 | 24 | THREE.OrbitControls = function ( object, domElement ) { 25 | 26 | this.object = object; 27 | this.domElement = ( domElement !== undefined ) ? domElement : document; 28 | 29 | // API 30 | 31 | // Set to false to disable this control 32 | this.enabled = true; 33 | 34 | // "target" sets the location of focus, where the control orbits around 35 | // and where it pans with respect to. 36 | this.target = new THREE.Vector3(); 37 | 38 | // center is old, deprecated; use "target" instead 39 | this.center = this.target; 40 | 41 | // This option actually enables dollying in and out; left as "zoom" for 42 | // backwards compatibility 43 | this.noZoom = false; 44 | this.zoomSpeed = 1.0; 45 | 46 | // Limits to how far you can dolly in and out 47 | this.minDistance = 0; 48 | this.maxDistance = Infinity; 49 | 50 | // Set to true to disable this control 51 | this.noRotate = false; 52 | this.rotateSpeed = 1.0; 53 | 54 | // Set to true to disable this control 55 | this.noPan = false; 56 | this.keyPanSpeed = 7.0; // pixels moved per arrow key push 57 | 58 | // Set to true to automatically rotate around the target 59 | this.autoRotate = false; 60 | this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60 61 | 62 | // How far you can orbit vertically, upper and lower limits. 63 | // Range is 0 to Math.PI radians. 64 | this.minPolarAngle = 0; // radians 65 | this.maxPolarAngle = Math.PI; // radians 66 | 67 | // Set to true to disable use of the keys 68 | this.noKeys = false; 69 | 70 | // The four arrow keys 71 | this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 }; 72 | 73 | //////////// 74 | // internals 75 | 76 | var scope = this; 77 | 78 | var EPS = 0.000001; 79 | 80 | var rotateStart = new THREE.Vector2(); 81 | var rotateEnd = new THREE.Vector2(); 82 | var rotateDelta = new THREE.Vector2(); 83 | 84 | var panStart = new THREE.Vector2(); 85 | var panEnd = new THREE.Vector2(); 86 | var panDelta = new THREE.Vector2(); 87 | var panOffset = new THREE.Vector3(); 88 | 89 | var offset = new THREE.Vector3(); 90 | 91 | var dollyStart = new THREE.Vector2(); 92 | var dollyEnd = new THREE.Vector2(); 93 | var dollyDelta = new THREE.Vector2(); 94 | 95 | var phiDelta = 0; 96 | var thetaDelta = 0; 97 | var scale = 1; 98 | var pan = new THREE.Vector3(); 99 | 100 | var lastPosition = new THREE.Vector3(); 101 | 102 | var STATE = { NONE : -1, ROTATE : 0, DOLLY : 1, PAN : 2, TOUCH_ROTATE : 3, TOUCH_DOLLY : 4, TOUCH_PAN : 5 }; 103 | 104 | var state = STATE.NONE; 105 | 106 | // for reset 107 | 108 | this.target0 = this.target.clone(); 109 | this.position0 = this.object.position.clone(); 110 | 111 | // events 112 | 113 | var changeEvent = { type: 'change' }; 114 | var startEvent = { type: 'start'}; 115 | var endEvent = { type: 'end'}; 116 | 117 | this.rotateLeft = function ( angle ) { 118 | 119 | if ( angle === undefined ) { 120 | 121 | angle = getAutoRotationAngle(); 122 | 123 | } 124 | 125 | thetaDelta -= angle; 126 | 127 | }; 128 | 129 | this.rotateUp = function ( angle ) { 130 | 131 | if ( angle === undefined ) { 132 | 133 | angle = getAutoRotationAngle(); 134 | 135 | } 136 | 137 | phiDelta -= angle; 138 | 139 | }; 140 | 141 | // pass in distance in world space to move left 142 | this.panLeft = function ( distance ) { 143 | 144 | var te = this.object.matrix.elements; 145 | 146 | // get X column of matrix 147 | panOffset.set( te[ 0 ], te[ 1 ], te[ 2 ] ); 148 | panOffset.multiplyScalar( - distance ); 149 | 150 | pan.add( panOffset ); 151 | 152 | }; 153 | 154 | // pass in distance in world space to move up 155 | this.panUp = function ( distance ) { 156 | 157 | var te = this.object.matrix.elements; 158 | 159 | // get Y column of matrix 160 | panOffset.set( te[ 4 ], te[ 5 ], te[ 6 ] ); 161 | panOffset.multiplyScalar( distance ); 162 | 163 | pan.add( panOffset ); 164 | 165 | }; 166 | 167 | // pass in x,y of change desired in pixel space, 168 | // right and down are positive 169 | this.pan = function ( deltaX, deltaY ) { 170 | 171 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 172 | 173 | if ( scope.object.fov !== undefined ) { 174 | 175 | // perspective 176 | var position = scope.object.position; 177 | var offset = position.clone().sub( scope.target ); 178 | var targetDistance = offset.length(); 179 | 180 | // half of the fov is center to top of screen 181 | targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 ); 182 | 183 | // we actually don't use screenWidth, since perspective camera is fixed to screen height 184 | scope.panLeft( 2 * deltaX * targetDistance / element.clientHeight ); 185 | scope.panUp( 2 * deltaY * targetDistance / element.clientHeight ); 186 | 187 | } else if ( scope.object.top !== undefined ) { 188 | 189 | // orthographic 190 | scope.panLeft( deltaX * (scope.object.right - scope.object.left) / element.clientWidth ); 191 | scope.panUp( deltaY * (scope.object.top - scope.object.bottom) / element.clientHeight ); 192 | 193 | } else { 194 | 195 | // camera neither orthographic or perspective 196 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); 197 | 198 | } 199 | 200 | }; 201 | 202 | this.dollyIn = function ( dollyScale ) { 203 | 204 | if ( dollyScale === undefined ) { 205 | 206 | dollyScale = getZoomScale(); 207 | 208 | } 209 | 210 | scale /= dollyScale; 211 | 212 | }; 213 | 214 | this.dollyOut = function ( dollyScale ) { 215 | 216 | if ( dollyScale === undefined ) { 217 | 218 | dollyScale = getZoomScale(); 219 | 220 | } 221 | 222 | scale *= dollyScale; 223 | 224 | }; 225 | 226 | this.update = function () { 227 | 228 | var position = this.object.position; 229 | 230 | offset.copy( position ).sub( this.target ); 231 | 232 | // angle from z-axis around y-axis 233 | 234 | var theta = Math.atan2( offset.x, offset.z ); 235 | 236 | // angle from y-axis 237 | 238 | var phi = Math.atan2( Math.sqrt( offset.x * offset.x + offset.z * offset.z ), offset.y ); 239 | 240 | if ( this.autoRotate ) { 241 | 242 | this.rotateLeft( getAutoRotationAngle() ); 243 | 244 | } 245 | 246 | theta += thetaDelta; 247 | phi += phiDelta; 248 | 249 | // restrict phi to be between desired limits 250 | phi = Math.max( this.minPolarAngle, Math.min( this.maxPolarAngle, phi ) ); 251 | 252 | // restrict phi to be betwee EPS and PI-EPS 253 | phi = Math.max( EPS, Math.min( Math.PI - EPS, phi ) ); 254 | 255 | var radius = offset.length() * scale; 256 | 257 | // restrict radius to be between desired limits 258 | radius = Math.max( this.minDistance, Math.min( this.maxDistance, radius ) ); 259 | 260 | // move target to panned location 261 | this.target.add( pan ); 262 | 263 | offset.x = radius * Math.sin( phi ) * Math.sin( theta ); 264 | offset.y = radius * Math.cos( phi ); 265 | offset.z = radius * Math.sin( phi ) * Math.cos( theta ); 266 | 267 | position.copy( this.target ).add( offset ); 268 | 269 | this.object.lookAt( this.target ); 270 | 271 | thetaDelta = 0; 272 | phiDelta = 0; 273 | scale = 1; 274 | pan.set( 0, 0, 0 ); 275 | 276 | if ( lastPosition.distanceTo( this.object.position ) > 0 ) { 277 | 278 | this.dispatchEvent( changeEvent ); 279 | 280 | lastPosition.copy( this.object.position ); 281 | 282 | } 283 | 284 | }; 285 | 286 | 287 | this.reset = function () { 288 | 289 | state = STATE.NONE; 290 | 291 | this.target.copy( this.target0 ); 292 | this.object.position.copy( this.position0 ); 293 | 294 | this.update(); 295 | 296 | }; 297 | 298 | function getAutoRotationAngle() { 299 | 300 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; 301 | 302 | } 303 | 304 | function getZoomScale() { 305 | 306 | return Math.pow( 0.95, scope.zoomSpeed ); 307 | 308 | } 309 | 310 | function onMouseDown( event ) { 311 | 312 | if ( scope.enabled === false ) return; 313 | event.preventDefault(); 314 | 315 | if ( event.button === 0 ) { 316 | if ( scope.noRotate === true ) return; 317 | 318 | state = STATE.ROTATE; 319 | 320 | rotateStart.set( event.clientX, event.clientY ); 321 | 322 | } else if ( event.button === 1 ) { 323 | if ( scope.noZoom === true ) return; 324 | 325 | state = STATE.DOLLY; 326 | 327 | dollyStart.set( event.clientX, event.clientY ); 328 | 329 | } else if ( event.button === 2 ) { 330 | if ( scope.noPan === true ) return; 331 | 332 | state = STATE.PAN; 333 | 334 | panStart.set( event.clientX, event.clientY ); 335 | 336 | } 337 | 338 | scope.domElement.addEventListener( 'mousemove', onMouseMove, false ); 339 | scope.domElement.addEventListener( 'mouseup', onMouseUp, false ); 340 | scope.dispatchEvent( startEvent ); 341 | 342 | } 343 | 344 | function onMouseMove( event ) { 345 | 346 | if ( scope.enabled === false ) return; 347 | 348 | event.preventDefault(); 349 | 350 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 351 | 352 | if ( state === STATE.ROTATE ) { 353 | 354 | if ( scope.noRotate === true ) return; 355 | 356 | rotateEnd.set( event.clientX, event.clientY ); 357 | rotateDelta.subVectors( rotateEnd, rotateStart ); 358 | 359 | // rotating across whole screen goes 360 degrees around 360 | scope.rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed ); 361 | 362 | // rotating up and down along whole screen attempts to go 360, but limited to 180 363 | scope.rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed ); 364 | 365 | rotateStart.copy( rotateEnd ); 366 | 367 | } else if ( state === STATE.DOLLY ) { 368 | 369 | if ( scope.noZoom === true ) return; 370 | 371 | dollyEnd.set( event.clientX, event.clientY ); 372 | dollyDelta.subVectors( dollyEnd, dollyStart ); 373 | 374 | if ( dollyDelta.y > 0 ) { 375 | 376 | scope.dollyIn(); 377 | 378 | } else { 379 | 380 | scope.dollyOut(); 381 | 382 | } 383 | 384 | dollyStart.copy( dollyEnd ); 385 | 386 | } else if ( state === STATE.PAN ) { 387 | 388 | if ( scope.noPan === true ) return; 389 | 390 | panEnd.set( event.clientX, event.clientY ); 391 | panDelta.subVectors( panEnd, panStart ); 392 | 393 | scope.pan( panDelta.x, panDelta.y ); 394 | 395 | panStart.copy( panEnd ); 396 | 397 | } 398 | 399 | scope.update(); 400 | 401 | } 402 | 403 | function onMouseUp( /* event */ ) { 404 | 405 | if ( scope.enabled === false ) return; 406 | 407 | scope.domElement.removeEventListener( 'mousemove', onMouseMove, false ); 408 | scope.domElement.removeEventListener( 'mouseup', onMouseUp, false ); 409 | scope.dispatchEvent( endEvent ); 410 | state = STATE.NONE; 411 | 412 | } 413 | 414 | function onMouseWheel( event ) { 415 | 416 | if ( scope.enabled === false || scope.noZoom === true ) return; 417 | 418 | event.preventDefault(); 419 | 420 | var delta = 0; 421 | 422 | if ( event.wheelDelta !== undefined ) { // WebKit / Opera / Explorer 9 423 | 424 | delta = event.wheelDelta; 425 | 426 | } else if ( event.detail !== undefined ) { // Firefox 427 | 428 | delta = - event.detail; 429 | 430 | } 431 | 432 | if ( delta > 0 ) { 433 | 434 | scope.dollyOut(); 435 | 436 | } else { 437 | 438 | scope.dollyIn(); 439 | 440 | } 441 | 442 | scope.update(); 443 | scope.dispatchEvent( startEvent ); 444 | scope.dispatchEvent( endEvent ); 445 | 446 | } 447 | 448 | function onKeyDown( event ) { 449 | 450 | if ( scope.enabled === false || scope.noKeys === true || scope.noPan === true ) return; 451 | 452 | switch ( event.keyCode ) { 453 | 454 | case scope.keys.UP: 455 | scope.pan( 0, scope.keyPanSpeed ); 456 | scope.update(); 457 | break; 458 | 459 | case scope.keys.BOTTOM: 460 | scope.pan( 0, - scope.keyPanSpeed ); 461 | scope.update(); 462 | break; 463 | 464 | case scope.keys.LEFT: 465 | scope.pan( scope.keyPanSpeed, 0 ); 466 | scope.update(); 467 | break; 468 | 469 | case scope.keys.RIGHT: 470 | scope.pan( - scope.keyPanSpeed, 0 ); 471 | scope.update(); 472 | break; 473 | 474 | } 475 | 476 | } 477 | 478 | function touchstart( event ) { 479 | 480 | if ( scope.enabled === false ) return; 481 | 482 | switch ( event.touches.length ) { 483 | 484 | case 1: // one-fingered touch: rotate 485 | 486 | if ( scope.noRotate === true ) return; 487 | 488 | state = STATE.TOUCH_ROTATE; 489 | 490 | rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 491 | break; 492 | 493 | case 2: // two-fingered touch: dolly 494 | 495 | if ( scope.noZoom === true ) return; 496 | 497 | state = STATE.TOUCH_DOLLY; 498 | 499 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 500 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 501 | var distance = Math.sqrt( dx * dx + dy * dy ); 502 | dollyStart.set( 0, distance ); 503 | break; 504 | 505 | case 3: // three-fingered touch: pan 506 | 507 | if ( scope.noPan === true ) return; 508 | 509 | state = STATE.TOUCH_PAN; 510 | 511 | panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 512 | break; 513 | 514 | default: 515 | 516 | state = STATE.NONE; 517 | 518 | } 519 | 520 | scope.dispatchEvent( startEvent ); 521 | 522 | } 523 | 524 | function touchmove( event ) { 525 | 526 | if ( scope.enabled === false ) return; 527 | 528 | event.preventDefault(); 529 | event.stopPropagation(); 530 | 531 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 532 | 533 | switch ( event.touches.length ) { 534 | 535 | case 1: // one-fingered touch: rotate 536 | 537 | if ( scope.noRotate === true ) return; 538 | if ( state !== STATE.TOUCH_ROTATE ) return; 539 | 540 | rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 541 | rotateDelta.subVectors( rotateEnd, rotateStart ); 542 | 543 | // rotating across whole screen goes 360 degrees around 544 | scope.rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed ); 545 | // rotating up and down along whole screen attempts to go 360, but limited to 180 546 | scope.rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed ); 547 | 548 | rotateStart.copy( rotateEnd ); 549 | 550 | scope.update(); 551 | break; 552 | 553 | case 2: // two-fingered touch: dolly 554 | 555 | if ( scope.noZoom === true ) return; 556 | if ( state !== STATE.TOUCH_DOLLY ) return; 557 | 558 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 559 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 560 | var distance = Math.sqrt( dx * dx + dy * dy ); 561 | 562 | dollyEnd.set( 0, distance ); 563 | dollyDelta.subVectors( dollyEnd, dollyStart ); 564 | 565 | if ( dollyDelta.y > 0 ) { 566 | 567 | scope.dollyOut(); 568 | 569 | } else { 570 | 571 | scope.dollyIn(); 572 | 573 | } 574 | 575 | dollyStart.copy( dollyEnd ); 576 | 577 | scope.update(); 578 | break; 579 | 580 | case 3: // three-fingered touch: pan 581 | 582 | if ( scope.noPan === true ) return; 583 | if ( state !== STATE.TOUCH_PAN ) return; 584 | 585 | panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 586 | panDelta.subVectors( panEnd, panStart ); 587 | 588 | scope.pan( panDelta.x, panDelta.y ); 589 | 590 | panStart.copy( panEnd ); 591 | 592 | scope.update(); 593 | break; 594 | 595 | default: 596 | 597 | state = STATE.NONE; 598 | 599 | } 600 | 601 | } 602 | 603 | function touchend( /* event */ ) { 604 | 605 | if ( scope.enabled === false ) return; 606 | 607 | scope.dispatchEvent( endEvent ); 608 | state = STATE.NONE; 609 | 610 | } 611 | 612 | this.domElement.addEventListener( 'contextmenu', function ( event ) { event.preventDefault(); }, false ); 613 | this.domElement.addEventListener( 'mousedown', onMouseDown, false ); 614 | this.domElement.addEventListener( 'mousewheel', onMouseWheel, false ); 615 | this.domElement.addEventListener( 'DOMMouseScroll', onMouseWheel, false ); // firefox 616 | 617 | this.domElement.addEventListener( 'touchstart', touchstart, false ); 618 | this.domElement.addEventListener( 'touchend', touchend, false ); 619 | this.domElement.addEventListener( 'touchmove', touchmove, false ); 620 | 621 | window.addEventListener( 'keydown', onKeyDown, false ); 622 | 623 | }; 624 | 625 | THREE.OrbitControls.prototype = Object.create( THREE.EventDispatcher.prototype ); 626 | -------------------------------------------------------------------------------- /bin/osmthree.js: -------------------------------------------------------------------------------- 1 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o mid) { 84 | hash_value = (hash_value << 1) + 1; 85 | minLon = mid; 86 | } else { 87 | hash_value = (hash_value << 1) + 0; 88 | maxLon = mid; 89 | } 90 | } else { 91 | mid = (maxLat + minLat) / 2; 92 | if (latitude > mid) { 93 | hash_value = (hash_value << 1) + 1; 94 | minLat = mid; 95 | } else { 96 | hash_value = (hash_value << 1) + 0; 97 | maxLat = mid; 98 | } 99 | } 100 | 101 | bits++; 102 | bitsTotal++; 103 | if (bits === 5) { 104 | var code = BASE32_CODES[hash_value]; 105 | chars.push(code); 106 | bits = 0; 107 | hash_value = 0; 108 | } 109 | } 110 | return chars.join(''); 111 | }; 112 | 113 | /** 114 | * Encode Integer 115 | * 116 | * Create a Geohash out of a latitude and longitude that is of 'bitDepth'. 117 | * 118 | * @param {Number} latitude 119 | * @param {Number} longitude 120 | * @param {Number} bitDepth 121 | * @returns {Number} 122 | */ 123 | var encode_int = function (latitude, longitude, bitDepth) { 124 | 125 | bitDepth = bitDepth || 52; 126 | 127 | var bitsTotal = 0, 128 | maxLat = 90, 129 | minLat = -90, 130 | maxLon = 180, 131 | minLon = -180, 132 | mid, 133 | combinedBits = 0; 134 | 135 | while (bitsTotal < bitDepth) { 136 | combinedBits *= 2; 137 | if (bitsTotal % 2 === 0) { 138 | mid = (maxLon + minLon) / 2; 139 | if (longitude > mid) { 140 | combinedBits += 1; 141 | minLon = mid; 142 | } else { 143 | maxLon = mid; 144 | } 145 | } else { 146 | mid = (maxLat + minLat) / 2; 147 | if (latitude > mid) { 148 | combinedBits += 1; 149 | minLat = mid; 150 | } else { 151 | maxLat = mid; 152 | } 153 | } 154 | bitsTotal++; 155 | } 156 | return combinedBits; 157 | }; 158 | 159 | /** 160 | * Decode Bounding Box 161 | * 162 | * Decode hashString into a bound box matches it. Data returned in a four-element array: [minlat, minlon, maxlat, maxlon] 163 | * @param {String} hash_string 164 | * @returns {Array} 165 | */ 166 | var decode_bbox = function (hash_string) { 167 | var isLon = true, 168 | maxLat = 90, 169 | minLat = -90, 170 | maxLon = 180, 171 | minLon = -180, 172 | mid; 173 | 174 | var hashValue = 0; 175 | for (var i = 0, l = hash_string.length; i < l; i++) { 176 | var code = hash_string[i].toLowerCase(); 177 | hashValue = BASE32_CODES_DICT[code]; 178 | 179 | for (var bits = 4; bits >= 0; bits--) { 180 | var bit = (hashValue >> bits) & 1; 181 | if (isLon) { 182 | mid = (maxLon + minLon) / 2; 183 | if (bit === 1) { 184 | minLon = mid; 185 | } else { 186 | maxLon = mid; 187 | } 188 | } else { 189 | mid = (maxLat + minLat) / 2; 190 | if (bit === 1) { 191 | minLat = mid; 192 | } else { 193 | maxLat = mid; 194 | } 195 | } 196 | isLon = !isLon; 197 | } 198 | } 199 | return [minLat, minLon, maxLat, maxLon]; 200 | }; 201 | 202 | /** 203 | * Decode Bounding Box Integer 204 | * 205 | * Decode hash number into a bound box matches it. Data returned in a four-element array: [minlat, minlon, maxlat, maxlon] 206 | * @param {Number} hashInt 207 | * @param {Number} bitDepth 208 | * @returns {Array} 209 | */ 210 | var decode_bbox_int = function (hashInt, bitDepth) { 211 | 212 | bitDepth = bitDepth || 52; 213 | 214 | var maxLat = 90, 215 | minLat = -90, 216 | maxLon = 180, 217 | minLon = -180; 218 | 219 | var latBit = 0, lonBit = 0; 220 | var step = bitDepth / 2; 221 | 222 | for (var i = 0; i < step; i++) { 223 | 224 | lonBit = get_bit(hashInt, ((step - i) * 2) - 1); 225 | latBit = get_bit(hashInt, ((step - i) * 2) - 2); 226 | 227 | if (latBit === 0) { 228 | maxLat = (maxLat + minLat) / 2; 229 | } 230 | else { 231 | minLat = (maxLat + minLat) / 2; 232 | } 233 | 234 | if (lonBit === 0) { 235 | maxLon = (maxLon + minLon) / 2; 236 | } 237 | else { 238 | minLon = (maxLon + minLon) / 2; 239 | } 240 | } 241 | return [minLat, minLon, maxLat, maxLon]; 242 | }; 243 | 244 | function get_bit(bits, position) { 245 | return (bits / Math.pow(2, position)) & 0x01; 246 | } 247 | 248 | /** 249 | * Decode 250 | * 251 | * Decode a hash string into pair of latitude and longitude. A javascript object is returned with keys `latitude`, 252 | * `longitude` and `error`. 253 | * @param {String} hashString 254 | * @returns {Object} 255 | */ 256 | var decode = function (hashString) { 257 | var bbox = decode_bbox(hashString); 258 | var lat = (bbox[0] + bbox[2]) / 2; 259 | var lon = (bbox[1] + bbox[3]) / 2; 260 | var latErr = bbox[2] - lat; 261 | var lonErr = bbox[3] - lon; 262 | return {latitude: lat, longitude: lon, 263 | error: {latitude: latErr, longitude: lonErr}}; 264 | }; 265 | 266 | /** 267 | * Decode Integer 268 | * 269 | * Decode a hash number into pair of latitude and longitude. A javascript object is returned with keys `latitude`, 270 | * `longitude` and `error`. 271 | * @param {Number} hash_int 272 | * @param {Number} bitDepth 273 | * @returns {Object} 274 | */ 275 | var decode_int = function (hash_int, bitDepth) { 276 | var bbox = decode_bbox_int(hash_int, bitDepth); 277 | var lat = (bbox[0] + bbox[2]) / 2; 278 | var lon = (bbox[1] + bbox[3]) / 2; 279 | var latErr = bbox[2] - lat; 280 | var lonErr = bbox[3] - lon; 281 | return {latitude: lat, longitude: lon, 282 | error: {latitude: latErr, longitude: lonErr}}; 283 | }; 284 | 285 | /** 286 | * Neighbor 287 | * 288 | * Find neighbor of a geohash string in certain direction. Direction is a two-element array, i.e. [1,0] means north, [-1,-1] means southwest. 289 | * direction [lat, lon], i.e. 290 | * [1,0] - north 291 | * [1,1] - northeast 292 | * ... 293 | * @param {String} hashString 294 | * @param {Array} Direction as a 2D normalized vector. 295 | * @returns {String} 296 | */ 297 | var neighbor = function (hashString, direction) { 298 | var lonLat = decode(hashString); 299 | var neighborLat = lonLat.latitude 300 | + direction[0] * lonLat.error.latitude * 2; 301 | var neighborLon = lonLat.longitude 302 | + direction[1] * lonLat.error.longitude * 2; 303 | return encode(neighborLat, neighborLon, hashString.length); 304 | }; 305 | 306 | /** 307 | * Neighbor Integer 308 | * 309 | * Find neighbor of a geohash integer in certain direction. Direction is a two-element array, i.e. [1,0] means north, [-1,-1] means southwest. 310 | * direction [lat, lon], i.e. 311 | * [1,0] - north 312 | * [1,1] - northeast 313 | * ... 314 | * @param {String} hash_string 315 | * @returns {Array} 316 | */ 317 | var neighbor_int = function(hash_int, direction, bitDepth) { 318 | bitDepth = bitDepth || 52; 319 | var lonlat = decode_int(hash_int, bitDepth); 320 | var neighbor_lat = lonlat.latitude + direction[0] * lonlat.error.latitude * 2; 321 | var neighbor_lon = lonlat.longitude + direction[1] * lonlat.error.longitude * 2; 322 | return encode_int(neighbor_lat, neighbor_lon, bitDepth); 323 | }; 324 | 325 | /** 326 | * Neighbors 327 | * 328 | * Returns all neighbors' hashstrings clockwise from north around to northwest 329 | * 7 0 1 330 | * 6 x 2 331 | * 5 4 3 332 | * @param {String} hash_string 333 | * @returns {encoded neighborHashList|Array} 334 | */ 335 | var neighbors = function(hash_string){ 336 | 337 | var hashstringLength = hash_string.length; 338 | 339 | var lonlat = decode(hash_string); 340 | var lat = lonlat.latitude; 341 | var lon = lonlat.longitude; 342 | var latErr = lonlat.error.latitude * 2; 343 | var lonErr = lonlat.error.longitude * 2; 344 | 345 | var neighbor_lat, 346 | neighbor_lon; 347 | 348 | var neighborHashList = [ 349 | encodeNeighbor(1,0), 350 | encodeNeighbor(1,1), 351 | encodeNeighbor(0,1), 352 | encodeNeighbor(-1,1), 353 | encodeNeighbor(-1,0), 354 | encodeNeighbor(-1,-1), 355 | encodeNeighbor(0,-1), 356 | encodeNeighbor(1,-1) 357 | ]; 358 | 359 | function encodeNeighbor(neighborLatDir, neighborLonDir){ 360 | neighbor_lat = lat + neighborLatDir * latErr; 361 | neighbor_lon = lon + neighborLonDir * lonErr; 362 | return encode(neighbor_lat, neighbor_lon, hashstringLength); 363 | } 364 | 365 | return neighborHashList; 366 | }; 367 | 368 | /** 369 | * Neighbors Integer 370 | * 371 | * Returns all neighbors' hash integers clockwise from north around to northwest 372 | * 7 0 1 373 | * 6 x 2 374 | * 5 4 3 375 | * @param {Number} hash_int 376 | * @param {Number} bitDepth 377 | * @returns {encode_int'd neighborHashIntList|Array} 378 | */ 379 | var neighbors_int = function(hash_int, bitDepth){ 380 | 381 | bitDepth = bitDepth || 52; 382 | 383 | var lonlat = decode_int(hash_int, bitDepth); 384 | var lat = lonlat.latitude; 385 | var lon = lonlat.longitude; 386 | var latErr = lonlat.error.latitude * 2; 387 | var lonErr = lonlat.error.longitude * 2; 388 | 389 | var neighbor_lat, 390 | neighbor_lon; 391 | 392 | var neighborHashIntList = [ 393 | encodeNeighbor_int(1,0), 394 | encodeNeighbor_int(1,1), 395 | encodeNeighbor_int(0,1), 396 | encodeNeighbor_int(-1,1), 397 | encodeNeighbor_int(-1,0), 398 | encodeNeighbor_int(-1,-1), 399 | encodeNeighbor_int(0,-1), 400 | encodeNeighbor_int(1,-1) 401 | ]; 402 | 403 | function encodeNeighbor_int(neighborLatDir, neighborLonDir){ 404 | neighbor_lat = lat + neighborLatDir * latErr; 405 | neighbor_lon = lon + neighborLonDir * lonErr; 406 | return encode_int(neighbor_lat, neighbor_lon, bitDepth); 407 | } 408 | 409 | return neighborHashIntList; 410 | }; 411 | 412 | 413 | /** 414 | * Bounding Boxes 415 | * 416 | * Return all the hashString between minLat, minLon, maxLat, maxLon in numberOfChars 417 | * @param {Number} minLat 418 | * @param {Number} minLon 419 | * @param {Number} maxLat 420 | * @param {Number} maxLon 421 | * @param {Number} numberOfChars 422 | * @returns {bboxes.hashList|Array} 423 | */ 424 | var bboxes = function (minLat, minLon, maxLat, maxLon, numberOfChars) { 425 | numberOfChars = numberOfChars || 9; 426 | 427 | var hashSouthWest = encode(minLat, minLon, numberOfChars); 428 | var hashNorthEast = encode(maxLat, maxLon, numberOfChars); 429 | 430 | var latLon = decode(hashSouthWest); 431 | 432 | var perLat = latLon.error.latitude * 2; 433 | var perLon = latLon.error.longitude * 2; 434 | 435 | var boxSouthWest = decode_bbox(hashSouthWest); 436 | var boxNorthEast = decode_bbox(hashNorthEast); 437 | 438 | var latStep = Math.round((boxNorthEast[0] - boxSouthWest[0]) / perLat); 439 | var lonStep = Math.round((boxNorthEast[1] - boxSouthWest[1]) / perLon); 440 | 441 | var hashList = []; 442 | 443 | for (var lat = 0; lat <= latStep; lat++) { 444 | for (var lon = 0; lon <= lonStep; lon++) { 445 | hashList.push(neighbor(hashSouthWest, [lat, lon])); 446 | } 447 | } 448 | 449 | return hashList; 450 | }; 451 | 452 | /** 453 | * Bounding Boxes Integer 454 | * 455 | * Return all the hash integers between minLat, minLon, maxLat, maxLon in bitDepth 456 | * @param {Number} minLat 457 | * @param {Number} minLon 458 | * @param {Number} maxLat 459 | * @param {Number} maxLon 460 | * @param {Number} bitDepth 461 | * @returns {bboxes_int.hashList|Array} 462 | */ 463 | var bboxes_int = function(minLat, minLon, maxLat, maxLon, bitDepth){ 464 | bitDepth = bitDepth || 52; 465 | 466 | var hashSouthWest = encode_int(minLat, minLon, bitDepth); 467 | var hashNorthEast = encode_int(maxLat, maxLon, bitDepth); 468 | 469 | var latlon = decode_int(hashSouthWest, bitDepth); 470 | 471 | var perLat = latlon.error.latitude * 2; 472 | var perLon = latlon.error.longitude * 2; 473 | 474 | var boxSouthWest = decode_bbox_int(hashSouthWest, bitDepth); 475 | var boxNorthEast = decode_bbox_int(hashNorthEast, bitDepth); 476 | 477 | var latStep = Math.round((boxNorthEast[0] - boxSouthWest[0])/perLat); 478 | var lonStep = Math.round((boxNorthEast[1] - boxSouthWest[1])/perLon); 479 | 480 | var hashList = []; 481 | 482 | for(var lat = 0; lat <= latStep; lat++){ 483 | for(var lon = 0; lon <= lonStep; lon++){ 484 | hashList.push(neighbor_int(hashSouthWest,[lat, lon], bitDepth)); 485 | } 486 | } 487 | 488 | return hashList; 489 | }; 490 | 491 | var geohash = { 492 | 'ENCODE_AUTO': ENCODE_AUTO, 493 | 'encode': encode, 494 | 'encode_uint64': encode_int, // keeping for backwards compatibility, will deprecate 495 | 'encode_int': encode_int, 496 | 'decode': decode, 497 | 'decode_int': decode_int, 498 | 'decode_uint64': decode_int, // keeping for backwards compatibility, will deprecate 499 | 'decode_bbox': decode_bbox, 500 | 'decode_bbox_uint64': decode_bbox_int, // keeping for backwards compatibility, will deprecate 501 | 'decode_bbox_int': decode_bbox_int, 502 | 'neighbor': neighbor, 503 | 'neighbor_int': neighbor_int, 504 | 'neighbors': neighbors, 505 | 'neighbors_int': neighbors_int, 506 | 'bboxes': bboxes, 507 | 'bboxes_int': bboxes_int 508 | }; 509 | 510 | module.exports = geohash; 511 | 512 | },{}],2:[function(require,module,exports){ 513 | 514 | 515 | function constructor( readyCallback, scale, origin, options ) { 516 | 517 | var 518 | options = options || {}, 519 | _readyCallback = readyCallback, 520 | _scale = scale, 521 | _origin = lonLatToWorld( origin[0], origin[1] ), 522 | _meshCallback = options.meshCallback || createMesh, 523 | _defaultColor = options.defaultColor || 0xf0f0f0; 524 | 525 | 526 | this.build = function( items ) { 527 | 528 | var bldg, currVerLen, 529 | mats = [], 530 | ids = []; 531 | //geom = new THREE.Geometry(); 532 | 533 | for ( var i=0, len=items.length; i < len; i++ ) { 534 | bldg = makeBldgGeom( items[i] ); 535 | if (bldg) { 536 | _readyCallback( _meshCallback.call( this, bldg, items[i] ) ); 537 | } 538 | //currVerLen = geom.vertices.length; 539 | //geom.vertices = geom.vertices.concat( bldg.vertices ); 540 | //geom.faces = geom.faces.concat( updateFaces( bldg.faces, currVerLen ) ); 541 | //mats = mats.concat( bldg.materials ); 542 | // Is this really necessary? 543 | //for ( var j = 0, fLen = bldg.faces.length; j < fLen; j++ ) { 544 | //ids.push( i ); 545 | //} 546 | } 547 | 548 | // TODO Create the mesh object and any necessary material objects 549 | //_scene.add( new THREE.Mesh( geom, new THREE.MeshNormalMaterial() ) ); 550 | 551 | } 552 | 553 | 554 | function updateFaces( faces, len ) { 555 | for ( var i=0, flen = faces.length; i < flen; i++ ) { 556 | faces[i].a += len; 557 | faces[i].b += len; 558 | faces[i].c += len; 559 | if ( faces[i].d ) { 560 | faces[i].d += len; 561 | } 562 | } 563 | return faces; 564 | } 565 | 566 | 567 | function createMesh( geom, osmData ) { 568 | // return new THREE.Mesh( geom, new THREE.MeshLambertMaterial() ); 569 | var face, 570 | mats = [], 571 | wci = 0, 572 | rci = 0; 573 | if ( osmData.wallColor ) { 574 | mats.push( new THREE.MeshLambertMaterial( {color: osmData.wallColor }) ); 575 | } else { 576 | mats.push( new THREE.MeshLambertMaterial( {color: _defaultColor } ) ); 577 | } 578 | if ( osmData.roofColor ) { 579 | mats.push( new THREE.MeshLambertMaterial( {color: osmData.roofColor }) ); 580 | rci = 1; 581 | } 582 | for ( var i=0, len=geom.faces.length; i < len; i++ ) { 583 | face = geom.faces[i]; 584 | ( face.normal.y === 1 ) ? face.materialIndex = rci 585 | : face.materialIndex = wci; 586 | } 587 | var m = new THREE.Mesh( geom, new THREE.MeshFaceMaterial( mats ) ); 588 | m.footprint = osmData.footprint; 589 | return m; 590 | } 591 | 592 | 593 | function lonLatToWorld( lon, lat ) { 594 | var x, y, pointX, pointY, latRad, mercN, 595 | worldWidth = 40075000, 596 | worldHeight = 40008000; 597 | 598 | x = ( lon + 180 ) * ( worldWidth / 360); 599 | latRad = lat*Math.PI/180; 600 | mercN = Math.log( Math.tan((Math.PI/4)+(latRad/2))); 601 | y = (worldHeight/2)-(worldHeight*mercN/(2*Math.PI)); 602 | return [ x, y ] 603 | } 604 | 605 | 606 | function lonLatToScene( lon, lat ) { 607 | var point = lonLatToWorld( lon, lat ); 608 | // This looks weird, and it is kind of a hack, but it's done because of the way THREE.ExtrudeGeometry converts 609 | // Vector2 x,y coordinates into x,z coordinates in Vector3 objects. +x +y goes to -z,-x. This effectively rotates 610 | // the geometries, putting them in the correct quadrant. Doing an actual rotation might be cleaner, but, well. 611 | return new THREE.Vector2( _origin[1] - point[1], _origin[0] - point[0] ); 612 | } 613 | 614 | 615 | function makeBldgGeom( item ) { 616 | // Create a path 617 | var pointX, pointY, extrudePath, eg, 618 | path, shapes, 619 | bldgHeight = item.height, 620 | pathPoints = []; 621 | 622 | for ( var i = 0, last = item.footprint.length-1; i < last; i+=2 ) { 623 | pathPoints.push( lonLatToScene( item.footprint[i+1], item.footprint[i] ) ); 624 | } 625 | 626 | path = new THREE.Path( pathPoints ); 627 | shapes = path.toShapes(); // isCCW, noHoles 628 | 629 | extrudePath = new THREE.CurvePath(); 630 | extrudePath.add( new THREE.LineCurve3( new THREE.Vector3(0,0,0), new THREE.Vector3(0,bldgHeight,0) ) ); 631 | 632 | eg = new THREE.ExtrudeGeometry( shapes, { 633 | extrudePath: extrudePath, 634 | material: 0 635 | }) 636 | 637 | return eg; 638 | 639 | } 640 | 641 | } 642 | 643 | module.exports = constructor; 644 | },{}],3:[function(require,module,exports){ 645 | 646 | var YARD_TO_METER = 0.9144, 647 | FOOT_TO_METER = 0.3048, 648 | INCH_TO_METER = 0.0254, 649 | METERS_PER_LEVEL = 3, 650 | DEFAULT_HEIGHT = 5, 651 | 652 | clockwise = 'CW', 653 | counterClockwise = 'CCW'; 654 | 655 | 656 | module.exports = { 657 | 658 | YARD_TO_METER: YARD_TO_METER, 659 | FOOT_TO_METER: FOOT_TO_METER, 660 | INCH_TO_METER: INCH_TO_METER, 661 | METERS_PER_LEVEL: METERS_PER_LEVEL, 662 | DEFAULT_HEIGHT: DEFAULT_HEIGHT, 663 | 664 | clockwise: clockwise, 665 | counterClockwise: counterClockwise, 666 | 667 | // detect winding direction: clockwise or counter clockwise 668 | getWinding: function(points) { 669 | var x1, y1, x2, y2, 670 | a = 0, 671 | i, il; 672 | for (i = 0, il = points.length-3; i < il; i += 2) { 673 | x1 = points[i]; 674 | y1 = points[i+1]; 675 | x2 = points[i+2]; 676 | y2 = points[i+3]; 677 | a += x1*y2 - x2*y1; 678 | } 679 | return (a/2) > 0 ? this.clockwise : this.counterClockwise; 680 | }, 681 | 682 | // enforce a polygon winding direcetion. Needed for proper backface culling. 683 | makeWinding: function(points, direction) { 684 | var winding = this.getWinding(points); 685 | if (winding === direction) { 686 | return points; 687 | } 688 | var revPoints = []; 689 | for (var i = points.length-2; i >= 0; i -= 2) { 690 | revPoints.push(points[i], points[i+1]); 691 | } 692 | return revPoints; 693 | }, 694 | 695 | toMeters: function(str) { 696 | str = '' + str; 697 | var value = parseFloat(str); 698 | if (value === str) { 699 | return value <<0; 700 | } 701 | if (~str.indexOf('m')) { 702 | return value <<0; 703 | } 704 | if (~str.indexOf('yd')) { 705 | return value*this.YARD_TO_METER <<0; 706 | } 707 | if (~str.indexOf('ft')) { 708 | return value*this.FOOT_TO_METER <<0; 709 | } 710 | if (~str.indexOf('\'')) { 711 | var parts = str.split('\''); 712 | var res = parts[0]*this.FOOT_TO_METER + parts[1]*this.INCH_TO_METER; 713 | return res <<0; 714 | } 715 | return value <<0; 716 | }, 717 | 718 | getRadius: function(points) { 719 | var minLat = 90, maxLat = -90; 720 | for (var i = 0, il = points.length; i < il; i += 2) { 721 | minLat = min(minLat, points[i]); 722 | maxLat = max(maxLat, points[i]); 723 | } 724 | 725 | return (maxLat-minLat) / RAD * 6378137 / 2 <<0; // 6378137 = Earth radius 726 | }, 727 | 728 | materialColors: { 729 | brick:'#cc7755', 730 | bronze:'#ffeecc', 731 | canvas:'#fff8f0', 732 | concrete:'#999999', 733 | copper:'#a0e0d0', 734 | glass:'#e8f8f8', 735 | gold:'#ffcc00', 736 | plants:'#009933', 737 | metal:'#aaaaaa', 738 | panel:'#fff8f0', 739 | plaster:'#999999', 740 | roof_tiles:'#f08060', 741 | silver:'#cccccc', 742 | slate:'#666666', 743 | stone:'#996666', 744 | tar_paper:'#333333', 745 | wood:'#deb887' 746 | }, 747 | 748 | baseMaterials: { 749 | asphalt:'tar_paper', 750 | bitumen:'tar_paper', 751 | block:'stone', 752 | bricks:'brick', 753 | glas:'glass', 754 | glassfront:'glass', 755 | grass:'plants', 756 | masonry:'stone', 757 | granite:'stone', 758 | panels:'panel', 759 | paving_stones:'stone', 760 | plastered:'plaster', 761 | rooftiles:'roof_tiles', 762 | roofingfelt:'tar_paper', 763 | sandstone:'stone', 764 | sheet:'canvas', 765 | sheets:'canvas', 766 | shingle:'tar_paper', 767 | shingles:'tar_paper', 768 | slates:'slate', 769 | steel:'metal', 770 | tar:'tar_paper', 771 | tent:'canvas', 772 | thatch:'plants', 773 | tile:'roof_tiles', 774 | tiles:'roof_tiles' 775 | }, 776 | 777 | // cardboard 778 | // eternit 779 | // limestone 780 | // straw 781 | 782 | getMaterialColor: function(str) { 783 | str = str.toLowerCase(); 784 | if (str[0] === '#') { 785 | return str; 786 | } 787 | return this.materialColors[this.baseMaterials[str] || str] || null; 788 | }, 789 | 790 | // aligns and cleans up properties in place 791 | alignProperties: function(prop) { 792 | var item = {}; 793 | 794 | prop = prop || {}; 795 | 796 | item.height = this.toMeters(prop.height); 797 | if (!item.height) { 798 | if (prop['building:height']) { 799 | item.height = this.toMeters(prop['building:height']); 800 | } 801 | if (prop.levels) { 802 | item.height = prop.levels*this.METERS_PER_LEVEL <<0; 803 | } 804 | if (prop['building:levels']) { 805 | item.height = prop['building:levels']*this.METERS_PER_LEVEL <<0; 806 | } 807 | if (!item.height) { 808 | item.height = DEFAULT_HEIGHT; 809 | } 810 | } 811 | 812 | item.minHeight = this.toMeters(prop.min_height); 813 | if (!item.min_height) { 814 | if (prop['building:min_height']) { 815 | item.minHeight = this.toMeters(prop['building:min_height']); 816 | } 817 | if (prop.min_level) { 818 | item.minHeight = prop.min_level*this.METERS_PER_LEVEL <<0; 819 | } 820 | if (prop['building:min_level']) { 821 | item.minHeight = prop['building:min_level']*this.METERS_PER_LEVEL <<0; 822 | } 823 | } 824 | 825 | item.wallColor = prop.wallColor || prop.color; 826 | if (!item.wallColor) { 827 | if (prop.color) { 828 | item.wallColor = prop.color; 829 | } 830 | if (prop['building:material']) { 831 | item.wallColor = this.getMaterialColor(prop['building:material']); 832 | } 833 | if (prop['building:facade:material']) { 834 | item.wallColor = this.getMaterialColor(prop['building:facade:material']); 835 | } 836 | if (prop['building:cladding']) { 837 | item.wallColor = this.getMaterialColor(prop['building:cladding']); 838 | } 839 | // wall color 840 | if (prop['building:color']) { 841 | item.wallColor = prop['building:color']; 842 | } 843 | if (prop['building:colour']) { 844 | item.wallColor = prop['building:colour']; 845 | } 846 | } 847 | 848 | item.roofColor = prop.roofColor; 849 | if (!item.roofColor) { 850 | if (prop['roof:material']) { 851 | item.roofColor = this.getMaterialColor(prop['roof:material']); 852 | } 853 | if (prop['building:roof:material']) { 854 | item.roofColor = this.getMaterialColor(prop['building:roof:material']); 855 | } 856 | // roof color 857 | if (prop['roof:color']) { 858 | item.roofColor = prop['roof:color']; 859 | } 860 | if (prop['roof:colour']) { 861 | item.roofColor = prop['roof:colour']; 862 | } 863 | if (prop['building:roof:color']) { 864 | item.roofColor = prop['building:roof:color']; 865 | } 866 | if (prop['building:roof:colour']) { 867 | item.roofColor = prop['building:roof:colour']; 868 | } 869 | } 870 | 871 | switch (prop['building:shape']) { 872 | case 'cone': 873 | case 'cylinder': 874 | item.shape = prop['building:shape']; 875 | break; 876 | 877 | case 'dome': 878 | item.shape = 'dome'; 879 | break; 880 | 881 | case 'sphere': 882 | item.shape = 'cylinder'; 883 | break; 884 | } 885 | 886 | if ((prop['roof:shape'] === 'cone' || prop['roof:shape'] === 'dome') && prop['roof:height']) { 887 | item.shape = 'cylinder'; 888 | item.roofShape = prop['roof:shape']; 889 | item.roofHeight = this.toMeters(prop['roof:height']); 890 | } 891 | 892 | if (item.roofHeight) { 893 | item.height = max(0, item.height-item.roofHeight); 894 | } else { 895 | item.roofHeight = 0; 896 | } 897 | 898 | return item; 899 | } 900 | }; 901 | 902 | },{}],4:[function(require,module,exports){ 903 | 904 | 905 | // Loader is responsible for fetching data from the Overpass API 906 | 907 | function constructor() { 908 | 909 | var OSM_XAPI_URL = 'http://overpass-api.de/api/interpreter?data=[out:json];(way[%22building%22]({s},{w},{n},{e});node(w);way[%22building:part%22=%22yes%22]({s},{w},{n},{e});node(w);relation[%22building%22]({s},{w},{n},{e});way(r);node(w););out;'; 910 | 911 | var req = new XMLHttpRequest(); 912 | 913 | function xhr(url, param, callback) { 914 | 915 | url = url.replace(/\{ *([\w_]+) *\}/g, function(tag, key) { 916 | return param[key] || tag; 917 | }); 918 | 919 | req.onerror = function() { 920 | req.status = 500; 921 | req.statusText = 'Error'; 922 | }; 923 | 924 | req.ontimeout = function() { 925 | req.status = 408; 926 | req.statusText = 'Timeout'; 927 | }; 928 | 929 | req.onprogress = function() { 930 | }; 931 | 932 | req.onload = function() { 933 | req.status = 200; 934 | req.statusText = 'Ok'; 935 | }; 936 | 937 | req.onreadystatechange = function() { 938 | if (req.readyState !== 4) { 939 | return; 940 | } 941 | if (!req.status || req.status < 200 || req.status > 299) { 942 | return; 943 | } 944 | if (callback && req.responseText) { 945 | callback( JSON.parse(req.responseText) ); 946 | } 947 | } 948 | 949 | req.open('GET', url); 950 | req.send(null); 951 | 952 | }; 953 | 954 | 955 | // load fetches data from the Overpass API for the given bounding box 956 | // PARAMETERS: 957 | // bbox --> a four float array consisting of [ , , , ], 958 | // callback --> a callback function to be called when the data is returned 959 | this.load = function( bbox, callback ) { 960 | var params = { 961 | e: bbox[2], 962 | n: bbox[3], 963 | s: bbox[1], 964 | w: bbox[0] 965 | } 966 | xhr( OSM_XAPI_URL, params, callback ); 967 | } 968 | 969 | } 970 | 971 | module.exports = constructor; 972 | },{}],5:[function(require,module,exports){ 973 | 974 | var Loader = require("./loader.js"), 975 | Parser = require("./parser.js"), 976 | Builder = require("./builder.js"), 977 | ngeo = require('ngeohash'); // TEMPORARY! Or maybe not? 978 | 979 | // makeBuildings fetches data from the Overpass API and builds three.js 3d models of buildings for everything 980 | // found within the given bounding box. 981 | // PARAMETERS: 982 | // callback --> a function that gets called when a building mesh is completely built and ready to be added to a scene 983 | // bbox --> a four float array specifying the min and max latitude and longitude coordinates whithin which to fetch 984 | // buildings. [ , , , ] (Note: It's lon,lat not lat,lon) 985 | // params --> an object that contains optional parameters to further control how the buildings are created. See the source code. 986 | function makeBuildings( callback, bbox, params ) { 987 | 988 | var 989 | buildOpts = {}, 990 | params = params || {}, 991 | origin = params.origin || [ bbox[0], bbox[1] ], // an array, [ lon, lat ], describing the poisition of the scene's origin 992 | units = params.units || 'meter', // 'meter', 'foot', 'inch', or 'millimeter' 993 | scale = params.scale || 1.0, // float describing how much to scale the units for the scene 994 | onDataReady = params.onDataReady || false; // called when data is loaded from Overpass, before THREE objects are created 995 | 996 | buildOpts.mergeGeometry = params.mergeGeometry || false; // create one big Geometry and Mesh with all the buildings 997 | buildOpts.defaultColor = params.defaultColor || false; // most buildings will be this color - default is 0xF0F0F0 998 | buildOpts.meshFunction = params.meshFunction || false; // custom function for creating the THREE.Mesh objects 999 | 1000 | var 1001 | builder = new Builder( callback, scale, origin, buildOpts ), 1002 | parser = new Parser( builder.build, onDataReady ), 1003 | loader = new Loader(); 1004 | 1005 | loader.load( bbox, parser.parse ); 1006 | 1007 | } 1008 | 1009 | 1010 | // Just gets the building data from the Overpass API and calls the callback, passing in the building data. 1011 | function fetchBldgData( callback, bbox, params ) { 1012 | 1013 | var onDataReady = params.onDataReady || false, 1014 | parser = new Parser( callback, onDataReady ), 1015 | loader = new Loader(); 1016 | 1017 | loader.load( bbox, parser.parse ); 1018 | 1019 | } 1020 | 1021 | 1022 | // Given some building data, creates meshes and calls the callback when it's done 1023 | function buildBldgs( callback, buildingData, params ) { 1024 | 1025 | var buildOpts = {}, 1026 | scale = params.scale || 1.0, 1027 | origin = params.origin || findDefaultOrigin( buildingData ); 1028 | 1029 | buildOpts.mergeGeometry = params.mergeGeometry || false; 1030 | buildOpts.defaultColor = params.defaultColor || false; 1031 | buildOpts.meshFunction = params.meshFunction || false; 1032 | 1033 | var builder = new Builder( callback, scale, origin, buildOpts ); 1034 | 1035 | builder.build( buildingData ); 1036 | 1037 | } 1038 | 1039 | 1040 | // 1041 | function findDefaultOrigin( bldgs ) { 1042 | console.log( bldgs ); 1043 | return [ 0, 0 ]; 1044 | } 1045 | 1046 | 1047 | module.exports = { 1048 | makeBuildings: makeBuildings, 1049 | fetchBldgData: fetchBldgData, 1050 | buildBldgs: buildBldgs 1051 | } 1052 | 1053 | // Maybe put this in a separte wrapper file, included in a version for use in a non-NPM context 1054 | window.OSM3 = { 1055 | makeBuildings: makeBuildings, 1056 | fetchBldgData: fetchBldgData, 1057 | buildBldgs: buildBldgs 1058 | } 1059 | 1060 | window.ngeo = ngeo // TEMPORARY!!!!! 1061 | 1062 | // TODO Go back to making the first argument to makeBuildings a callback instead of a THREE.Scene object. 1063 | // Accept a THREE.Plane object as an optional argument, and then geohash from its XZ values (instead of lat-lon) to its Y values. 1064 | // Export more fine-grained functions/modules within OSMthree that allow control over what happens and when, e.g. with Promises. 1065 | // (should these maintain state? Probably not, they should accept arguments, I think. ) 1066 | 1067 | },{"./builder.js":2,"./loader.js":4,"./parser.js":6,"ngeohash":1}],6:[function(require,module,exports){ 1068 | 1069 | var importer = require('./importer.js' ); 1070 | 1071 | function constructor( finalCallback, filterCallback ) { 1072 | 1073 | var _nodes = {}, 1074 | _ways = {}, 1075 | _relations = {}, 1076 | MAP_DATA = []; 1077 | 1078 | 1079 | function isBuilding(data) { 1080 | var tags = data.tags; 1081 | return (tags && !tags.landuse && 1082 | (tags.building || tags['building:part']) && (!tags.layer || tags.layer >= 0)); 1083 | } 1084 | 1085 | 1086 | function getRelationWays(members) { 1087 | var m, outer, inner = []; 1088 | for (var i = 0, il = members.length; i < il; i++) { 1089 | m = members[i]; 1090 | if (m.type !== 'way' || !_ways[m.ref]) { 1091 | continue; 1092 | } 1093 | if (!m.role || m.role === 'outer') { 1094 | outer = _ways[m.ref]; 1095 | continue; 1096 | } 1097 | if (m.role === 'inner' || m.role === 'enclave') { 1098 | inner.push(_ways[m.ref]); 1099 | continue; 1100 | } 1101 | } 1102 | 1103 | // if (outer && outer.tags) { 1104 | if (outer) { // allows tags to be attached to relation - instead of outer way 1105 | return { outer:outer, inner:inner }; 1106 | } 1107 | } 1108 | 1109 | 1110 | function getFootprint(points) { 1111 | if (!points) { 1112 | return; 1113 | } 1114 | 1115 | var footprint = [], p; 1116 | for (var i = 0, il = points.length; i < il; i++) { 1117 | p = _nodes[ points[i] ]; 1118 | footprint.push(p[0], p[1]); 1119 | } 1120 | 1121 | // do not close polygon yet 1122 | if (footprint[footprint.length-2] !== footprint[0] && footprint[footprint.length-1] !== footprint[1]) { 1123 | footprint.push(footprint[0], footprint[1]); 1124 | } 1125 | 1126 | // can't span a polygon with just 2 points (+ start & end) 1127 | if (footprint.length < 8) { 1128 | return; 1129 | } 1130 | 1131 | return footprint; 1132 | } 1133 | 1134 | 1135 | 1136 | function mergeItems(dst, src) { 1137 | for (var p in src) { 1138 | if (src.hasOwnProperty(p)) { 1139 | dst[p] = src[p]; 1140 | } 1141 | } 1142 | return dst; 1143 | } 1144 | 1145 | 1146 | function filterItem(item, footprint) { 1147 | var res = importer.alignProperties(item.tags); 1148 | res.tags = item.tags; // Keeping the raw tags too 1149 | if (item.id) { 1150 | res.id = item.id; 1151 | } 1152 | 1153 | if (footprint) { 1154 | res.footprint = importer.makeWinding(footprint, importer.clockwise); 1155 | } 1156 | 1157 | if (res.shape === 'cone' || res.shape === 'cylinder') { 1158 | res.radius = importer.getRadius(res.footprint); 1159 | } 1160 | 1161 | return res; 1162 | } 1163 | 1164 | 1165 | function processNode(node) { 1166 | _nodes[node.id] = [node.lat, node.lon]; 1167 | } 1168 | 1169 | 1170 | function processWay(way) { 1171 | if (isBuilding(way)) { 1172 | var item, footprint; 1173 | if ( footprint = getFootprint(way.nodes) ) { 1174 | item = filterItem(way, footprint); 1175 | MAP_DATA.push(item); 1176 | } 1177 | return; 1178 | } 1179 | 1180 | var tags = way.tags; 1181 | if (!tags || (!tags.highway && !tags.railway && !tags.landuse)) { // TODO: add more filters 1182 | _ways[way.id] = way; 1183 | } 1184 | } 1185 | 1186 | 1187 | function processRelation(relation) { 1188 | var relationWays, outerWay, holes = [], 1189 | item, relItem, outerFootprint, innerFootprint; 1190 | if (!isBuilding(relation) || 1191 | (relation.tags.type !== 'multipolygon' && relation.tags.type !== 'building') ) { 1192 | return; 1193 | } 1194 | 1195 | if ((relationWays = getRelationWays(relation.members))) { 1196 | relItem = filterItem(relation); 1197 | if ((outerWay = relationWays.outer)) { 1198 | if (outerFootprint = getFootprint(outerWay.nodes)) { 1199 | item = filterItem(outerWay, outerFootprint); 1200 | for (var i = 0, il = relationWays.inner.length; i < il; i++) { 1201 | if ((innerFootprint = getFootprint(relationWays.inner[i].nodes))) { 1202 | holes.push( importer.makeWinding(innerFootprint, importer.counterClockwise) ); 1203 | } 1204 | } 1205 | if (holes.length) { 1206 | item.holes = holes; 1207 | } 1208 | MAP_DATA.push( mergeItems(item, relItem) ); 1209 | } 1210 | } 1211 | } 1212 | } 1213 | 1214 | 1215 | this.parse = function( osmData ) { 1216 | var item, buildData; 1217 | for ( var i = 0, len = osmData.elements.length; i < len; i++ ) { 1218 | item = osmData.elements[i]; 1219 | switch ( item.type ) { 1220 | case 'node': processNode( item ); break; 1221 | case 'way': processWay( item ); break; 1222 | case 'relation': processRelation( item ); break; 1223 | } 1224 | } 1225 | ( filterCallback ) ? buildData = filterCallback.call( this, MAP_DATA ) 1226 | : buildData = MAP_DATA; 1227 | finalCallback.apply( this, [ buildData ] ); 1228 | } 1229 | 1230 | 1231 | } 1232 | 1233 | module.exports = constructor; 1234 | },{"./importer.js":3}]},{},[5]) --------------------------------------------------------------------------------