├── .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])
--------------------------------------------------------------------------------