/** * Copyright (C) 2015 OSM Buildings, Jan Marsch * A JavaScript library for visualizing building geometry on interactive maps. * @osmbuildings, http://osmbuildings.org */ //****** file: prefix.js ****** (function (global) { 'use strict'; //****** file: shortcuts.js ****** // object access shortcuts var m = Math, exp = m.exp, log = m.log, sin = m.sin, cos = m.cos, tan = m.tan, atan = m.atan, atan2 = m.atan2, min = m.min, max = m.max, sqrt = m.sqrt, ceil = m.ceil, floor = m.floor, round = m.round, pow = m.pow; var mapObj; // polyfills var Int32Array = Int32Array || Array, Uint8Array = Uint8Array || Array; var IS_IOS = /iP(ad|hone|od)/g.test(navigator.userAgent); var IS_MSIE = !!~navigator.userAgent.indexOf('Trident'); var requestAnimFrame = (global.requestAnimationFrame && !IS_IOS && !IS_MSIE) ? global.requestAnimationFrame : function (callback) { callback(); }; //****** file: Color.debug.js ****** var Color = (function (window) { var w3cColors = { aqua: '#00ffff', black: '#000000', blue: '#0000ff', fuchsia: '#ff00ff', gray: '#808080', grey: '#808080', green: '#008000', lime: '#00ff00', maroon: '#800000', navy: '#000080', olive: '#808000', orange: '#ffa500', purple: '#800080', red: '#ff0000', silver: '#c0c0c0', teal: '#008080', white: '#ffffff', yellow: '#ffff00' }; function hue2rgb(p, q, t) { if (t < 0) t += 1; if (t > 1) t -= 1; if (t < 1 / 6) return p + (q - p) * 6 * t; if (t < 1 / 2) return q; if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; return p; } function clamp(v, max) { return Math.min(max, Math.max(0, v)); } var Color = function (h, s, l, a) { this.H = h; this.S = s; this.L = l; this.A = a; }; /* * str can be in any of these: * #0099ff rgb(64, 128, 255) rgba(64, 128, 255, 0.5) */ Color.parse = function (str) { var r = 0, g = 0, b = 0, a = 1, m; str = ('' + str).toLowerCase(); str = w3cColors[str] || str; if ((m = str.match(/^#(\w{2})(\w{2})(\w{2})$/))) { r = parseInt(m[1], 16); g = parseInt(m[2], 16); b = parseInt(m[3], 16); } else if ((m = str.match(/rgba?\((\d+)\D+(\d+)\D+(\d+)(\D+([\d.]+))?\)/))) { r = parseInt(m[1], 10); g = parseInt(m[2], 10); b = parseInt(m[3], 10); a = m[4] ? parseFloat(m[5]) : 1; } else { return; } return this.fromRGBA(r, g, b, a); }; Color.fromRGBA = function (r, g, b, a) { if (typeof r === 'object') { g = r.g / 255; b = r.b / 255; a = r.a; r = r.r / 255; } else { r /= 255; g /= 255; b /= 255; } var max = Math.max(r, g, b), min = Math.min(r, g, b), h, s, l = (max + min) / 2, d = max - min; if (!d) { h = s = 0; // achromatic } else { s = l > 0.5 ? d / (2 - max - min) : d / (max + min); switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; } h *= 60; } return new Color(h, s, l, a); }; Color.prototype = { toRGBA: function () { var h = clamp(this.H, 360), s = clamp(this.S, 1), l = clamp(this.L, 1), rgba = {a: clamp(this.A, 1)}; // achromatic if (s === 0) { rgba.r = l; rgba.g = l; rgba.b = l; } else { var q = l < 0.5 ? l * (1 + s) : l + s - l * s, p = 2 * l - q; h /= 360; rgba.r = hue2rgb(p, q, h + 1 / 3); rgba.g = hue2rgb(p, q, h); rgba.b = hue2rgb(p, q, h - 1 / 3); } return { r: Math.round(rgba.r * 255), g: Math.round(rgba.g * 255), b: Math.round(rgba.b * 255), a: rgba.a }; }, toString: function () { var rgba = this.toRGBA(); if (rgba.a === 1) { return '#' + ((1 << 24) + (rgba.r << 16) + (rgba.g << 8) + rgba.b).toString(16).slice(1, 7); } return 'rgba(' + [rgba.r, rgba.g, rgba.b, rgba.a.toFixed(2)].join(',') + ')'; }, hue: function (h) { return new Color(this.H * h, this.S, this.L, this.A); }, saturation: function (s) { return new Color(this.H, this.S * s, this.L, this.A); }, lightness: function (l) { return new Color(this.H, this.S, this.L * l, this.A); }, alpha: function (a) { return new Color(this.H, this.S, this.L, this.A * a); } }; return Color; }(this)); //****** file: SunPosition.js ****** // calculations are based on http://aa.quae.nl/en/reken/zonpositie.html // code credits to Vladimir Agafonkin (@mourner) var getSunPosition = (function () { var m = Math, PI = m.PI, sin = m.sin, cos = m.cos, tan = m.tan, asin = m.asin, atan = m.atan2; var rad = PI / 180, dayMs = 1000 * 60 * 60 * 24, J1970 = 2440588, J2000 = 2451545, e = rad * 23.4397; // obliquity of the Earth function toJulian(date) { return date.valueOf() / dayMs - 0.5 + J1970; } function toDays(date) { return toJulian(date) - J2000; } function getRightAscension(l, b) { return atan(sin(l) * cos(e) - tan(b) * sin(e), cos(l)); } function getDeclination(l, b) { return asin(sin(b) * cos(e) + cos(b) * sin(e) * sin(l)); } function getAzimuth(H, phi, dec) { return atan(sin(H), cos(H) * sin(phi) - tan(dec) * cos(phi)); } function getAltitude(H, phi, dec) { return asin(sin(phi) * sin(dec) + cos(phi) * cos(dec) * cos(H)); } function getSiderealTime(d, lw) { return rad * (280.16 + 360.9856235 * d) - lw; } function getSolarMeanAnomaly(d) { return rad * (357.5291 + 0.98560028 * d); } function getEquationOfCenter(M) { return rad * (1.9148 * sin(M) + 0.0200 * sin(2 * M) + 0.0003 * sin(3 * M)); } function getEclipticLongitude(M, C) { var P = rad * 102.9372; // perihelion of the Earth return M + C + P + PI; } return function getSunPosition(date, lat, lon) { var lw = rad * -lon, phi = rad * lat, d = toDays(date), M = getSolarMeanAnomaly(d), C = getEquationOfCenter(M), L = getEclipticLongitude(M, C), D = getDeclination(L, 0), A = getRightAscension(L, 0), t = getSiderealTime(d, lw), H = t - A; return { altitude: getAltitude(H, phi, D), azimuth: getAzimuth(H, phi, D) - PI / 2 // origin: north }; }; }()); //****** file: GeoJSON.js ****** var GeoJSON = (function () { var METERS_PER_LEVEL = 3; var materialColors = { brick: '#cc7755', bronze: '#ffeecc', canvas: '#fff8f0', concrete: '#999999', copper: '#a0e0d0', glass: '#e8f8f8', gold: '#ffcc00', plants: '#009933', metal: '#aaaaaa', panel: '#fff8f0', plaster: '#999999', roof_tiles: '#f08060', silver: '#cccccc', slate: '#666666', stone: '#996666', tar_paper: '#333333', wood: '#deb887' }; var baseMaterials = { asphalt: 'tar_paper', bitumen: 'tar_paper', block: 'stone', bricks: 'brick', glas: 'glass', glassfront: 'glass', grass: 'plants', masonry: 'stone', granite: 'stone', panels: 'panel', paving_stones: 'stone', plastered: 'plaster', rooftiles: 'roof_tiles', roofingfelt: 'tar_paper', sandstone: 'stone', sheet: 'canvas', sheets: 'canvas', shingle: 'tar_paper', shingles: 'tar_paper', slates: 'slate', steel: 'metal', tar: 'tar_paper', tent: 'canvas', thatch: 'plants', tile: 'roof_tiles', tiles: 'roof_tiles' }; // cardboard // eternit // limestone // straw function getMaterialColor(str) { str = str.toLowerCase(); if (str[0] === '#') { return str; } return materialColors[baseMaterials[str] || str] || null; } var WINDING_CLOCKWISE = 'CW'; var WINDING_COUNTER_CLOCKWISE = 'CCW'; // detect winding direction: clockwise or counter clockwise function getWinding(points) { var x1, y1, x2, y2, a = 0, i, il; for (i = 0, il = points.length - 3; i < il; i += 2) { x1 = points[i]; y1 = points[i + 1]; x2 = points[i + 2]; y2 = points[i + 3]; a += x1 * y2 - x2 * y1; } return (a / 2) > 0 ? WINDING_CLOCKWISE : WINDING_COUNTER_CLOCKWISE; } // enforce a polygon winding direcetion. Needed for proper backface culling. function makeWinding(points, direction) { var winding = getWinding(points); if (winding === direction) { return points; } var revPoints = []; for (var i = points.length - 2; i >= 0; i -= 2) { revPoints.push(points[i], points[i + 1]); } return revPoints; } function alignProperties(prop) { var item = {}; prop = prop || {}; item.height = prop.height || (prop.levels ? prop.levels * METERS_PER_LEVEL : DEFAULT_HEIGHT); item.minHeight = prop.minHeight || (prop.minLevel ? prop.minLevel * METERS_PER_LEVEL : 0); var wallColor = prop.material ? getMaterialColor(prop.material) : (prop.wallColor || prop.color); if (wallColor) { item.wallColor = wallColor; } var roofColor = prop.roofMaterial ? getMaterialColor(prop.roofMaterial) : prop.roofColor; if (roofColor) { item.roofColor = roofColor; } switch (prop.shape) { case 'cylinder': case 'cone': case 'dome': case 'sphere': item.shape = prop.shape; item.isRotational = true; break; case 'pyramid': item.shape = prop.shape; break; } switch (prop.roofShape) { case 'cone': case 'dome': item.roofShape = prop.roofShape; item.isRotational = true; break; case 'pyramid': item.roofShape = prop.roofShape; break; } if (item.roofShape && prop.roofHeight) { item.roofHeight = prop.roofHeight; item.height = max(0, item.height - item.roofHeight); } else { item.roofHeight = 0; } return item; } function getGeometries(geometry) { var i, il, polygon, geometries = [], sub; switch (geometry.type) { case 'GeometryCollection': geometries = []; for (i = 0, il = geometry.geometries.length; i < il; i++) { if ((sub = getGeometries(geometry.geometries[i]))) { geometries.push.apply(geometries, sub); } } return geometries; case 'MultiPolygon': geometries = []; for (i = 0, il = geometry.coordinates.length; i < il; i++) { if ((sub = getGeometries({type: 'Polygon', coordinates: geometry.coordinates[i]}))) { geometries.push.apply(geometries, sub); } } return geometries; case 'Polygon': polygon = geometry.coordinates; break; default: return []; } var j, jl, p, lat = 1, lon = 0, outer = [], inner = []; p = polygon[0]; for (i = 0, il = p.length; i < il; i++) { outer.push(p[i][lat], p[i][lon]); } outer = makeWinding(outer, WINDING_CLOCKWISE); for (i = 0, il = polygon.length - 1; i < il; i++) { p = polygon[i + 1]; inner[i] = []; for (j = 0, jl = p.length; j < jl; j++) { inner[i].push(p[j][lat], p[j][lon]); } inner[i] = makeWinding(inner[i], WINDING_COUNTER_CLOCKWISE); } return [{ outer: outer, inner: inner.length ? inner : null }]; } function clone(obj) { var res = {}; for (var p in obj) { if (obj.hasOwnProperty(p)) { res[p] = obj[p]; } } return res; } return { read: function (geojson) { if (!geojson || geojson.type !== 'FeatureCollection') { return []; } var collection = geojson.features, i, il, j, jl, res = [], feature, geometries, baseItem, item; for (i = 0, il = collection.length; i < il; i++) { feature = collection[i]; if (feature.type !== 'Feature' || onEach(feature) === false) { continue; } baseItem = alignProperties(feature.properties); geometries = getGeometries(feature.geometry); for (j = 0, jl = geometries.length; j < jl; j++) { item = clone(baseItem); item.footprint = geometries[j].outer; if (item.isRotational) { item.radius = getLonDelta(item.footprint); } if (geometries[j].inner) { item.holes = geometries[j].inner; } if (feature.id || feature.properties.id) { item.id = feature.id || feature.properties.id; } if (feature.properties.relationId) { item.relationId = feature.properties.relationId; } res.push(item); // TODO: clone base properties! } } return res; } }; }()); //****** file: variables.js ****** var VERSION = '0.2.2b', ATTRIBUTION = '© OSM Buildings', DATA_SRC = 'http://{s}.data.osmbuildings.org/0.2/{k}/tile/{z}/{x}/{y}.json', ////////////////geojson 数据 PI = Math.PI, HALF_PI = PI / 2, QUARTER_PI = PI / 4, MAP_TILE_SIZE = 256, // map tile size in pixels DATA_TILE_SIZE = 0.0075, // data tile size in geo coordinates, smaller: less data to load but more requests ZOOM, MAP_SIZE, MIN_ZOOM = 8, LAT = 'latitude', LON = 'longitude', TRUE = true, FALSE = false, WIDTH = 0, HEIGHT = 0, CENTER_X = 0, CENTER_Y = 0, ORIGIN_X = 0, ORIGIN_Y = 0, WALL_COLOR = Color.parse('rgba(200, 190, 180)'), ALT_COLOR = WALL_COLOR.lightness(0.8), ROOF_COLOR = WALL_COLOR.lightness(1.2), WALL_COLOR_STR = '' + WALL_COLOR, ALT_COLOR_STR = '' + ALT_COLOR, ROOF_COLOR_STR = '' + ROOF_COLOR, PIXEL_PER_DEG = 0, ZOOM_FACTOR = 1, MAX_HEIGHT, // taller buildings will be cut to this DEFAULT_HEIGHT = 5, CAM_X, CAM_Y, CAM_Z = 450, isZooming; //****** file: geometry.js ****** function getDistance(p1, p2) { var dx = p1.x - p2.x, dy = p1.y - p2.y; return dx * dx + dy * dy; } //是否可旋转 function isRotational(polygon) { var length = polygon.length; if (length < 16) { return false; } var i; var minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity; for (i = 0; i < length - 1; i += 2) { minX = Math.min(minX, polygon[i]); maxX = Math.max(maxX, polygon[i]); minY = Math.min(minY, polygon[i + 1]); maxY = Math.max(maxY, polygon[i + 1]); } var width = maxX - minX, height = (maxY - minY), ratio = width / height; if (ratio < 0.85 || ratio > 1.15) { return false; } var center = {x: minX + width / 2, y: minY + height / 2}, radius = (width + height) / 4, sqRadius = radius * radius; for (i = 0; i < length - 1; i += 2) { var dist = getDistance({x: polygon[i], y: polygon[i + 1]}, center); if (dist / sqRadius < 0.8 || dist / sqRadius > 1.2) { return false; } } return true; } function getSquareSegmentDistance(px, py, p1x, p1y, p2x, p2y) { var dx = p2x - p1x, dy = p2y - p1y, t; if (dx !== 0 || dy !== 0) { t = ((px - p1x) * dx + (py - p1y) * dy) / (dx * dx + dy * dy); if (t > 1) { p1x = p2x; p1y = p2y; } else if (t > 0) { p1x += dx * t; p1y += dy * t; } } dx = px - p1x; dy = py - p1y; return dx * dx + dy * dy; } function simplifyPolygon(buffer) { var sqTolerance = 2, len = buffer.length / 2, markers = new Uint8Array(len), first = 0, last = len - 1, i, maxSqDist, sqDist, index, firstStack = [], lastStack = [], newBuffer = []; markers[first] = markers[last] = 1; while (last) { maxSqDist = 0; for (i = first + 1; i < last; i++) { sqDist = getSquareSegmentDistance( buffer[i * 2], buffer[i * 2 + 1], buffer[first * 2], buffer[first * 2 + 1], buffer[last * 2], buffer[last * 2 + 1] ); if (sqDist > maxSqDist) { index = i; maxSqDist = sqDist; } } if (maxSqDist > sqTolerance) { markers[index] = 1; firstStack.push(first); lastStack.push(index); firstStack.push(index); lastStack.push(last); } first = firstStack.pop(); last = lastStack.pop(); } for (i = 0; i < len; i++) { if (markers[i]) { newBuffer.push(buffer[i * 2], buffer[i * 2 + 1]); } } return newBuffer; } function getCenter(footprint) { var minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity; for (var i = 0, il = footprint.length - 3; i < il; i += 2) { minX = min(minX, footprint[i]); maxX = max(maxX, footprint[i]); minY = min(minY, footprint[i + 1]); maxY = max(maxY, footprint[i + 1]); } return {x: minX + (maxX - minX) / 2 << 0, y: minY + (maxY - minY) / 2 << 0}; } var EARTH_RADIUS = 6378137; function getLonDelta(footprint) { var minLon = 180, maxLon = -180; for (var i = 0, il = footprint.length; i < il; i += 2) { minLon = min(minLon, footprint[i + 1]); maxLon = max(maxLon, footprint[i + 1]); } return (maxLon - minLon) / 2; } //****** file: functions.js ****** function rad(deg) { return deg * PI / 180; } function deg(rad) { return rad / PI * 180; } function pixelToGeo(x, y) { var res = {}; x /= MAP_SIZE; y /= MAP_SIZE; res[LAT] = y <= 0 ? 90 : y >= 1 ? -90 : deg(2 * atan(exp(PI * (1 - 2 * y))) - HALF_PI); res[LON] = (x === 1 ? 1 : (x % 1 + 1) % 1) * 360 - 180; return res; } function geoToPixel(lat, lon) { var latLngProjection = isLatLngProjection(); if (latLngProjection) { var latitude = ((90 - lat) / 180), longitude = lon / 360 + 0.5; return { x: (longitude * MAP_SIZE) << 0, y: (latitude * MAP_SIZE / 2) << 0 }; } else { var latitude = min(1, max(0, 0.5 - (log(tan(QUARTER_PI + HALF_PI * lat / 180)) / PI) / 2)), longitude = lon / 360 + 0.5; return { x: (longitude * MAP_SIZE) << 0, y: (latitude * MAP_SIZE) << 0 }; } } function isLatLngProjection() { var code = mapObj.getProjection(); if (code.indexOf('3857') > -1) { return false; } if (code.indexOf('4326') > -1||code.indexOf('4490') > -1) { return true; } console.log('Only support EPSG:3857 and EPSG:4326(4490) projection!'); return false; } function fromRange(sVal, sMin, sMax, dMin, dMax) { sVal = min(max(sVal, sMin), sMax); var rel = (sVal - sMin) / (sMax - sMin), range = dMax - dMin; return min(max(dMin + rel * range, dMin), dMax); } function isVisible(polygon) { var maxX = WIDTH + ORIGIN_X, maxY = HEIGHT + ORIGIN_Y; // TODO: checking footprint is sufficient for visibility - NOT VALID FOR SHADOWS! for (var i = 0, il = polygon.length - 3; i < il; i += 2) { if (polygon[i] > ORIGIN_X && polygon[i] < maxX && polygon[i + 1] > ORIGIN_Y && polygon[i + 1] < maxY) { return true; } } return false; } //数据请求 原来是在线OSMBuilding OSM与上请求 Builldings Geojson 数据 //****** file: Request.js ****** var Request = (function () { //var cacheData = {}; //var cacheIndex = []; //var cacheSize = 0; //var maxCacheSize = 1024*1024 * 5; // 5MB // //function xhr(url, callback) { // if (cacheData[url]) { // if (callback) { // callback(cacheData[url]); // } // return; // } // // var req = new XMLHttpRequest(); // // req.onreadystatechange = function() { // if (req.readyState !== 4) { // return; // } // if (!req.status || req.status < 200 || req.status > 299) { // return; // } // if (callback && req.responseText) { // var responseText = req.responseText; // // cacheData[url] = responseText; // cacheIndex.push({ url: url, size: responseText.length }); // cacheSize += responseText.length; // // callback(responseText); // // while (cacheSize > maxCacheSize) { // var item = cacheIndex.shift(); // cacheSize -= item.size; // delete cacheData[item.url]; // } // } // }; // // req.open('GET', url); // req.send(null); // // return req; //} // //return { // loadJSON: function(url, callback) { // return xhr(url, function(responseText) { // var json; // try { // json = JSON.parse(responseText); // } catch(ex) {} // callback(json); // }); // } //}; }()); //****** file: Data.js ****** var Data = { loadedItems: {}, // maintain a list of cached items in order to avoid duplicates on tile borders items: [], getPixelFootprint: function (buffer) { var footprint = new Int32Array(buffer.length), px; for (var i = 0, il = buffer.length - 1; i < il; i += 2) { px = geoToPixel(buffer[i], buffer[i + 1]); footprint[i] = px.x; footprint[i + 1] = px.y; } footprint = simplifyPolygon(footprint); if (footprint.length < 8) { // 3 points & end==start (*2) return; } return footprint; }, resetItems: function () { this.items = []; this.loadedItems = {}; HitAreas.reset(); }, addRenderItems: function (data, allAreNew) { var item, scaledItem, id; var geojson = GeoJSON.read(data); for (var i = 0, il = geojson.length; i < il; i++) { item = geojson[i]; id = item.id || [item.footprint[0], item.footprint[1], item.height, item.minHeight].join(','); if (!this.loadedItems[id]) { if ((scaledItem = this.scale(item))) { scaledItem.scale = allAreNew ? 0 : 1; this.items.push(scaledItem); this.loadedItems[id] = 1; } } } fadeIn(); }, scale: function (item) { var res = {}, // TODO: calculate this on zoom change only zoomScale = 6 / pow(2, ZOOM - MIN_ZOOM); // TODO: consider using HEIGHT / (global.devicePixelRatio || 1) if (item.id) { res.id = item.id; } res.height = min(item.height / zoomScale, MAX_HEIGHT); res.minHeight = isNaN(item.minHeight) ? 0 : item.minHeight / zoomScale; if (res.minHeight > MAX_HEIGHT) { return; } res.footprint = this.getPixelFootprint(item.footprint); if (!res.footprint) { return; } res.center = getCenter(res.footprint); if (item.radius) { res.radius = item.radius * PIXEL_PER_DEG; } if (item.shape) { res.shape = item.shape; } if (item.roofShape) { res.roofShape = item.roofShape; } if ((res.roofShape === 'cone' || res.roofShape === 'dome') && !res.shape && isRotational(res.footprint)) { res.shape = 'cylinder'; } if (item.holes) { res.holes = []; var innerFootprint; for (var i = 0, il = item.holes.length; i < il; i++) { // TODO: simplify if ((innerFootprint = this.getPixelFootprint(item.holes[i]))) { res.holes.push(innerFootprint); } } } var color; if (item.wallColor) { if ((color = Color.parse(item.wallColor))) { color = color.alpha(ZOOM_FACTOR); res.altColor = '' + color.lightness(0.8); res.wallColor = '' + color; } } if (item.roofColor) { if ((color = Color.parse(item.roofColor))) { res.roofColor = '' + color.alpha(ZOOM_FACTOR); } } if (item.relationId) { res.relationId = item.relationId; } res.hitColor = HitAreas.idToColor(item.relationId || item.id); res.roofHeight = isNaN(item.roofHeight) ? 0 : item.roofHeight / zoomScale; if (res.height + res.roofHeight <= res.minHeight) { return; } return res; }, set: function (data) { this.isStatic = true; this.resetItems(); this._staticData = data; this.addRenderItems(this._staticData, true); }, load: function (src, key) { this.src = src || DATA_SRC.replace('{k}', (key || 'anonymous')); this.update(); }, update: function () { this.resetItems(); if (ZOOM < MIN_ZOOM) { return; } if (this.isStatic && this._staticData) { this.addRenderItems(this._staticData); return; } if (!this.src) { return; } var tileZoom = 13, tileSize = 256, zoomedTileSize = ZOOM > tileZoom ? tileSize << (ZOOM - tileZoom) : tileSize >> (tileZoom - ZOOM), minX = ORIGIN_X / zoomedTileSize << 0, minY = ORIGIN_Y / zoomedTileSize << 0, maxX = ceil((ORIGIN_X + WIDTH) / zoomedTileSize), maxY = ceil((ORIGIN_Y + HEIGHT) / zoomedTileSize), x, y; var scope = this; function callback(json) { scope.addRenderItems(json); } for (y = minY; y <= maxY; y++) { for (x = minX; x <= maxX; x++) { this.loadTile(x, y, tileZoom, callback); } } }, loadTile: function (x, y, zoom, callback) { var s = 'abcd'[(x + y) % 4]; var url = this.src.replace('{s}', s).replace('{x}', x).replace('{y}', y).replace('{z}', zoom); //return Request.loadJSON(url, callback); } }; //块状体 //****** file: Block.js ****** var Block = { draw: function (context, polygon, innerPolygons, height, minHeight, color, altColor, roofColor) { var i, il, roof = this._extrude(context, polygon, height, minHeight, color, altColor), innerRoofs = []; if (innerPolygons) { for (i = 0, il = innerPolygons.length; i < il; i++) { innerRoofs[i] = this._extrude(context, innerPolygons[i], height, minHeight, color, altColor); } } context.fillStyle = roofColor; context.beginPath(); this._ring(context, roof); if (innerPolygons) { for (i = 0, il = innerRoofs.length; i < il; i++) { this._ring(context, innerRoofs[i]); } } context.closePath(); context.stroke(); context.fill(); }, _extrude: function (context, polygon, height, minHeight, color, altColor) { var scale = CAM_Z / (CAM_Z - height), minScale = CAM_Z / (CAM_Z - minHeight), a = {x: 0, y: 0}, b = {x: 0, y: 0}, _a, _b, roof = []; for (var i = 0, il = polygon.length - 3; i < il; i += 2) { a.x = polygon[i] - ORIGIN_X; a.y = polygon[i + 1] - ORIGIN_Y; b.x = polygon[i + 2] - ORIGIN_X; b.y = polygon[i + 3] - ORIGIN_Y; _a = Buildings.project(a, scale); _b = Buildings.project(b, scale); if (minHeight) { a = Buildings.project(a, minScale); b = Buildings.project(b, minScale); } // backface culling check if ((b.x - a.x) * (_a.y - a.y) > (_a.x - a.x) * (b.y - a.y)) { // depending on direction, set wall shading if ((a.x < b.x && a.y < b.y) || (a.x > b.x && a.y > b.y)) { context.fillStyle = altColor; } else { context.fillStyle = color; } context.beginPath(); this._ring(context, [ b.x, b.y, a.x, a.y, _a.x, _a.y, _b.x, _b.y ]); context.closePath(); context.fill(); } roof[i] = _a.x; roof[i + 1] = _a.y; } return roof; }, _ring: function (context, polygon) { context.moveTo(polygon[0], polygon[1]); for (var i = 2, il = polygon.length - 1; i < il; i += 2) { context.lineTo(polygon[i], polygon[i + 1]); } }, simplified: function (context, polygon, innerPolygons) { context.beginPath(); this._ringAbs(context, polygon); if (innerPolygons) { for (var i = 0, il = innerPolygons.length; i < il; i++) { this._ringAbs(context, innerPolygons[i]); } } context.closePath(); context.stroke(); context.fill(); }, _ringAbs: function (context, polygon) { context.moveTo(polygon[0] - ORIGIN_X, polygon[1] - ORIGIN_Y); for (var i = 2, il = polygon.length - 1; i < il; i += 2) { context.lineTo(polygon[i] - ORIGIN_X, polygon[i + 1] - ORIGIN_Y); } }, shadow: function (context, polygon, innerPolygons, height, minHeight) { var mode = null, a = {x: 0, y: 0}, b = {x: 0, y: 0}, _a, _b; for (var i = 0, il = polygon.length - 3; i < il; i += 2) { a.x = polygon[i] - ORIGIN_X; a.y = polygon[i + 1] - ORIGIN_Y; b.x = polygon[i + 2] - ORIGIN_X; b.y = polygon[i + 3] - ORIGIN_Y; _a = Shadows.project(a, height); _b = Shadows.project(b, height); if (minHeight) { a = Shadows.project(a, minHeight); b = Shadows.project(b, minHeight); } // mode 0: floor edges, mode 1: roof edges if ((b.x - a.x) * (_a.y - a.y) > (_a.x - a.x) * (b.y - a.y)) { if (mode === 1) { context.lineTo(a.x, a.y); } mode = 0; if (!i) { context.moveTo(a.x, a.y); } context.lineTo(b.x, b.y); } else { if (mode === 0) { context.lineTo(_a.x, _a.y); } mode = 1; if (!i) { context.moveTo(_a.x, _a.y); } context.lineTo(_b.x, _b.y); } } if (innerPolygons) { for (i = 0, il = innerPolygons.length; i < il; i++) { this._ringAbs(context, innerPolygons[i]); } } }, shadowMask: function (context, polygon, innerPolygons) { this._ringAbs(context, polygon); if (innerPolygons) { for (var i = 0, il = innerPolygons.length; i < il; i++) { this._ringAbs(context, innerPolygons[i]); } } }, hitArea: function (context, polygon, innerPolygons, height, minHeight, color) { var mode = null, a = {x: 0, y: 0}, b = {x: 0, y: 0}, scale = CAM_Z / (CAM_Z - height), minScale = CAM_Z / (CAM_Z - minHeight), _a, _b; context.fillStyle = color; context.beginPath(); for (var i = 0, il = polygon.length - 3; i < il; i += 2) { a.x = polygon[i] - ORIGIN_X; a.y = polygon[i + 1] - ORIGIN_Y; b.x = polygon[i + 2] - ORIGIN_X; b.y = polygon[i + 3] - ORIGIN_Y; _a = Buildings.project(a, scale); _b = Buildings.project(b, scale); if (minHeight) { a = Buildings.project(a, minScale); b = Buildings.project(b, minScale); } // mode 0: floor edges, mode 1: roof edges if ((b.x - a.x) * (_a.y - a.y) > (_a.x - a.x) * (b.y - a.y)) { if (mode === 1) { // mode is initially undefined context.lineTo(a.x, a.y); } mode = 0; if (!i) { context.moveTo(a.x, a.y); } context.lineTo(b.x, b.y); } else { if (mode === 0) { // mode is initially undefined context.lineTo(_a.x, _a.y); } mode = 1; if (!i) { context.moveTo(_a.x, _a.y); } context.lineTo(_b.x, _b.y); } } context.closePath(); context.fill(); } }; //圆柱体 //****** file: Cylinder.js ****** var Cylinder = { draw: function (context, center, radius, topRadius, height, minHeight, color, altColor, roofColor) { var c = {x: center.x - ORIGIN_X, y: center.y - ORIGIN_Y}, scale = CAM_Z / (CAM_Z - height), minScale = CAM_Z / (CAM_Z - minHeight), apex = Buildings.project(c, scale), a1, a2; topRadius *= scale; if (minHeight) { c = Buildings.project(c, minScale); radius = radius * minScale; } // common tangents for ground and roof circle var tangents = this._tangents(c, radius, apex, topRadius); // no tangents? top circle is inside bottom circle if (!tangents) { a1 = 1.5 * PI; a2 = 1.5 * PI; } else { a1 = atan2(tangents[0].y1 - c.y, tangents[0].x1 - c.x); a2 = atan2(tangents[1].y1 - c.y, tangents[1].x1 - c.x); } context.fillStyle = color; context.beginPath(); context.arc(apex.x, apex.y, topRadius, HALF_PI, a1, true); context.arc(c.x, c.y, radius, a1, HALF_PI); context.closePath(); context.fill(); context.fillStyle = altColor; context.beginPath(); context.arc(apex.x, apex.y, topRadius, a2, HALF_PI, true); context.arc(c.x, c.y, radius, HALF_PI, a2); context.closePath(); context.fill(); context.fillStyle = roofColor; this._circle(context, apex, topRadius); }, simplified: function (context, center, radius) { this._circle(context, {x: center.x - ORIGIN_X, y: center.y - ORIGIN_Y}, radius); }, shadow: function (context, center, radius, topRadius, height, minHeight) { var c = {x: center.x - ORIGIN_X, y: center.y - ORIGIN_Y}, apex = Shadows.project(c, height), p1, p2; if (minHeight) { c = Shadows.project(c, minHeight); } // common tangents for ground and roof circle var tangents = this._tangents(c, radius, apex, topRadius); // TODO: no tangents? roof overlaps everything near cam position if (tangents) { p1 = atan2(tangents[0].y1 - c.y, tangents[0].x1 - c.x); p2 = atan2(tangents[1].y1 - c.y, tangents[1].x1 - c.x); context.moveTo(tangents[1].x2, tangents[1].y2); context.arc(apex.x, apex.y, topRadius, p2, p1); context.arc(c.x, c.y, radius, p1, p2); } else { context.moveTo(c.x + radius, c.y); context.arc(c.x, c.y, radius, 0, 2 * PI); } }, shadowMask: function (context, center, radius) { var c = {x: center.x - ORIGIN_X, y: center.y - ORIGIN_Y}; context.moveTo(c.x + radius, c.y); context.arc(c.x, c.y, radius, 0, PI * 2); }, hitArea: function (context, center, radius, topRadius, height, minHeight, color) { var c = {x: center.x - ORIGIN_X, y: center.y - ORIGIN_Y}, scale = CAM_Z / (CAM_Z - height), minScale = CAM_Z / (CAM_Z - minHeight), apex = Buildings.project(c, scale), p1, p2; topRadius *= scale; if (minHeight) { c = Buildings.project(c, minScale); radius = radius * minScale; } // common tangents for ground and roof circle var tangents = this._tangents(c, radius, apex, topRadius); context.fillStyle = color; context.beginPath(); // TODO: no tangents? roof overlaps everything near cam position if (tangents) { p1 = atan2(tangents[0].y1 - c.y, tangents[0].x1 - c.x); p2 = atan2(tangents[1].y1 - c.y, tangents[1].x1 - c.x); context.moveTo(tangents[1].x2, tangents[1].y2); context.arc(apex.x, apex.y, topRadius, p2, p1); context.arc(c.x, c.y, radius, p1, p2); } else { context.moveTo(c.x + radius, c.y); context.arc(c.x, c.y, radius, 0, 2 * PI); } context.closePath(); context.fill(); }, _circle: function (context, center, radius) { context.beginPath(); context.arc(center.x, center.y, radius, 0, PI * 2); context.stroke(); context.fill(); }, // http://en.wikibooks.org/wiki/Algorithm_Implementation/Geometry/Tangents_between_two_circles _tangents: function (c1, r1, c2, r2) { var dx = c1.x - c2.x, dy = c1.y - c2.y, dr = r1 - r2, sqdist = (dx * dx) + (dy * dy); if (sqdist <= dr * dr) { return; } var dist = sqrt(sqdist), vx = -dx / dist, vy = -dy / dist, c = dr / dist, res = [], h, nx, ny; // Let A, B be the centers, and C, D be points at which the tangent // touches first and second circle, and n be the normal vector to it. // // We have the system: // n * n = 1 (n is a unit vector) // C = A + r1 * n // D = B + r2 * n // n * CD = 0 (common orthogonality) // // n * CD = n * (AB + r2*n - r1*n) = AB*n - (r1 -/+ r2) = 0, <=> // AB * n = (r1 -/+ r2), <=> // v * n = (r1 -/+ r2) / d, where v = AB/|AB| = AB/d // This is a linear equation in unknown vector n. // Now we're just intersecting a line with a circle: v*n=c, n*n=1 h = sqrt(max(0, 1 - c * c)); for (var sign = 1; sign >= -1; sign -= 2) { nx = vx * c - sign * h * vy; ny = vy * c + sign * h * vx; res.push({ x1: c1.x + r1 * nx << 0, y1: c1.y + r1 * ny << 0, x2: c2.x + r2 * nx << 0, y2: c2.y + r2 * ny << 0 }); } return res; } }; //椎体 //****** file: Pyramid.js ****** var Pyramid = { draw: function (context, polygon, center, height, minHeight, color, altColor) { var c = {x: center.x - ORIGIN_X, y: center.y - ORIGIN_Y}, scale = CAM_Z / (CAM_Z - height), minScale = CAM_Z / (CAM_Z - minHeight), apex = Buildings.project(c, scale), a = {x: 0, y: 0}, b = {x: 0, y: 0}; for (var i = 0, il = polygon.length - 3; i < il; i += 2) { a.x = polygon[i] - ORIGIN_X; a.y = polygon[i + 1] - ORIGIN_Y; b.x = polygon[i + 2] - ORIGIN_X; b.y = polygon[i + 3] - ORIGIN_Y; if (minHeight) { a = Buildings.project(a, minScale); b = Buildings.project(b, minScale); } // backface culling check if ((b.x - a.x) * (apex.y - a.y) > (apex.x - a.x) * (b.y - a.y)) { // depending on direction, set shading if ((a.x < b.x && a.y < b.y) || (a.x > b.x && a.y > b.y)) { context.fillStyle = altColor; } else { context.fillStyle = color; } context.beginPath(); this._triangle(context, a, b, apex); context.closePath(); context.fill(); } } }, _triangle: function (context, a, b, c) { context.moveTo(a.x, a.y); context.lineTo(b.x, b.y); context.lineTo(c.x, c.y); }, _ring: function (context, polygon) { context.moveTo(polygon[0] - ORIGIN_X, polygon[1] - ORIGIN_Y); for (var i = 2, il = polygon.length - 1; i < il; i += 2) { context.lineTo(polygon[i] - ORIGIN_X, polygon[i + 1] - ORIGIN_Y); } }, shadow: function (context, polygon, center, height, minHeight) { var a = {x: 0, y: 0}, b = {x: 0, y: 0}, c = {x: center.x - ORIGIN_X, y: center.y - ORIGIN_Y}, apex = Shadows.project(c, height); for (var i = 0, il = polygon.length - 3; i < il; i += 2) { a.x = polygon[i] - ORIGIN_X; a.y = polygon[i + 1] - ORIGIN_Y; b.x = polygon[i + 2] - ORIGIN_X; b.y = polygon[i + 3] - ORIGIN_Y; if (minHeight) { a = Shadows.project(a, minHeight); b = Shadows.project(b, minHeight); } // backface culling check if ((b.x - a.x) * (apex.y - a.y) > (apex.x - a.x) * (b.y - a.y)) { // depending on direction, set shading this._triangle(context, a, b, apex); } } }, shadowMask: function (context, polygon) { this._ring(context, polygon); }, hitArea: function (context, polygon, center, height, minHeight, color) { var c = {x: center.x - ORIGIN_X, y: center.y - ORIGIN_Y}, scale = CAM_Z / (CAM_Z - height), minScale = CAM_Z / (CAM_Z - minHeight), apex = Buildings.project(c, scale), a = {x: 0, y: 0}, b = {x: 0, y: 0}; context.fillStyle = color; context.beginPath(); for (var i = 0, il = polygon.length - 3; i < il; i += 2) { a.x = polygon[i] - ORIGIN_X; a.y = polygon[i + 1] - ORIGIN_Y; b.x = polygon[i + 2] - ORIGIN_X; b.y = polygon[i + 3] - ORIGIN_Y; if (minHeight) { a = Buildings.project(a, minScale); b = Buildings.project(b, minScale); } // backface culling check if ((b.x - a.x) * (apex.y - a.y) > (apex.x - a.x) * (b.y - a.y)) { this._triangle(context, a, b, apex); } } context.closePath(); context.fill(); } }; //****** file: Buildings.js ****** var Buildings = { project: function (p, m) { return { x: (p.x - CAM_X) * m + CAM_X << 0, y: (p.y - CAM_Y) * m + CAM_Y << 0 }; }, render: function () { var context = this.context; context.clearRect(0, 0, WIDTH, HEIGHT); // show on high zoom levels only and avoid rendering during zoom if (ZOOM < MIN_ZOOM || isZooming) { return; } var item, h, mh, sortCam = {x: CAM_X + ORIGIN_X, y: CAM_Y + ORIGIN_Y}, footprint, wallColor, altColor, roofColor, dataItems = Data.items; dataItems.sort(function (a, b) { return (a.minHeight - b.minHeight) || getDistance(b.center, sortCam) - getDistance(a.center, sortCam) || (b.height - a.height); }); for (var i = 0, il = dataItems.length; i < il; i++) { item = dataItems[i]; if (Simplified.isSimple(item)) { continue; } footprint = item.footprint; if (!isVisible(footprint)) { continue; } // when fading in, use a dynamic height h = item.scale < 1 ? item.height * item.scale : item.height; mh = 0; if (item.minHeight) { mh = item.scale < 1 ? item.minHeight * item.scale : item.minHeight; } wallColor = item.wallColor || WALL_COLOR_STR; altColor = item.altColor || ALT_COLOR_STR; roofColor = item.roofColor || ROOF_COLOR_STR; context.strokeStyle = altColor; switch (item.shape) { case 'cylinder': Cylinder.draw(context, item.center, item.radius, item.radius, h, mh, wallColor, altColor, roofColor); break; case 'cone': Cylinder.draw(context, item.center, item.radius, 0, h, mh, wallColor, altColor); break; case 'dome': Cylinder.draw(context, item.center, item.radius, item.radius / 2, h, mh, wallColor, altColor); break; case 'sphere': Cylinder.draw(context, item.center, item.radius, item.radius, h, mh, wallColor, altColor, roofColor); break; case 'pyramid': Pyramid.draw(context, footprint, item.center, h, mh, wallColor, altColor); break; default: Block.draw(context, footprint, item.holes, h, mh, wallColor, altColor, roofColor); } switch (item.roofShape) { case 'cone': Cylinder.draw(context, item.center, item.radius, 0, h + item.roofHeight, h, roofColor, '' + Color.parse(roofColor).lightness(0.9)); break; case 'dome': Cylinder.draw(context, item.center, item.radius, item.radius / 2, h + item.roofHeight, h, roofColor, '' + Color.parse(roofColor).lightness(0.9)); break; case 'pyramid': Pyramid.draw(context, footprint, item.center, h + item.roofHeight, h, roofColor, Color.parse(roofColor).lightness(0.9)); break; } } } }; //****** file: Simplified.js ****** var Simplified = { maxZoom: MIN_ZOOM + 2, maxHeight: 5, isSimple: function (item) { return (ZOOM <= this.maxZoom && item.height + item.roofHeight < this.maxHeight); }, render: function () { var context = this.context; context.clearRect(0, 0, WIDTH, HEIGHT); // show on high zoom levels only and avoid rendering during zoom if (ZOOM < MIN_ZOOM || isZooming || ZOOM > this.maxZoom) { return; } var item, footprint, dataItems = Data.items; for (var i = 0, il = dataItems.length; i < il; i++) { item = dataItems[i]; if (item.height >= this.maxHeight) { continue; } footprint = item.footprint; if (!isVisible(footprint)) { continue; } context.strokeStyle = item.altColor || ALT_COLOR_STR; context.fillStyle = item.roofColor || ROOF_COLOR_STR; switch (item.shape) { case 'cylinder': case 'cone': case 'dome': case 'sphere': Cylinder.simplified(context, item.center, item.radius); break; default: Block.simplified(context, footprint, item.holes); } } } }; //****** file: Shadows.js ****** var Shadows = { enabled: true, color: '#666666', blurColor: '#000000', blurSize: 15, date: new Date(), direction: {x: 0, y: 0}, project: function (p, h) { return { x: p.x + this.direction.x * h, y: p.y + this.direction.y * h }; }, render: function () { var context = this.context, screenCenter, sun, length, alpha; context.clearRect(0, 0, WIDTH, HEIGHT); // show on high zoom levels only and avoid rendering during zoom if (!this.enabled || ZOOM < MIN_ZOOM || isZooming) { return; } // TODO: calculate this just on demand screenCenter = pixelToGeo(CENTER_X + ORIGIN_X, CENTER_Y + ORIGIN_Y); sun = getSunPosition(this.date, screenCenter.latitude, screenCenter.longitude); if (sun.altitude <= 0) { return; } length = 1 / tan(sun.altitude); alpha = length < 5 ? 0.75 : 1 / length * 5; this.direction.x = cos(sun.azimuth) * length; this.direction.y = sin(sun.azimuth) * length; var i, il, item, h, mh, footprint, dataItems = Data.items; context.canvas.style.opacity = alpha / (ZOOM_FACTOR * 2); context.shadowColor = this.blurColor; context.shadowBlur = this.blurSize * (ZOOM_FACTOR / 2); context.fillStyle = this.color; context.beginPath(); for (i = 0, il = dataItems.length; i < il; i++) { item = dataItems[i]; footprint = item.footprint; if (!isVisible(footprint)) { continue; } // when fading in, use a dynamic height h = item.scale < 1 ? item.height * item.scale : item.height; mh = 0; if (item.minHeight) { mh = item.scale < 1 ? item.minHeight * item.scale : item.minHeight; } switch (item.shape) { case 'cylinder': Cylinder.shadow(context, item.center, item.radius, item.radius, h, mh); break; case 'cone': Cylinder.shadow(context, item.center, item.radius, 0, h, mh); break; case 'dome': Cylinder.shadow(context, item.center, item.radius, item.radius / 2, h, mh); break; case 'sphere': Cylinder.shadow(context, item.center, item.radius, item.radius, h, mh); break; case 'pyramid': Pyramid.shadow(context, footprint, item.center, h, mh); break; default: Block.shadow(context, footprint, item.holes, h, mh); } switch (item.roofShape) { case 'cone': Cylinder.shadow(context, item.center, item.radius, 0, h + item.roofHeight, h); break; case 'dome': Cylinder.shadow(context, item.center, item.radius, item.radius / 2, h + item.roofHeight, h); break; case 'pyramid': Pyramid.shadow(context, footprint, item.center, h + item.roofHeight, h); break; } } context.closePath(); context.fill(); context.shadowBlur = null; // now draw all the footprints as negative clipping mask context.globalCompositeOperation = 'destination-out'; context.beginPath(); for (i = 0, il = dataItems.length; i < il; i++) { item = dataItems[i]; footprint = item.footprint; if (!isVisible(footprint)) { continue; } // if object is hovered, there is no need to clip it's footprint if (item.minHeight) { continue; } switch (item.shape) { case 'cylinder': case 'cone': case 'dome': Cylinder.shadowMask(context, item.center, item.radius); break; default: Block.shadowMask(context, footprint, item.holes); } } context.fillStyle = '#00ff00'; context.fill(); context.globalCompositeOperation = 'source-over'; } }; //****** file: HitAreas.js ****** var HitAreas = { _idMapping: [null], reset: function () { this._idMapping = [null]; }, render: function () { if (this._timer) { return; } var self = this; this._timer = setTimeout(function () { self._timer = null; self._render(); }, 500); }, _render: function () { var context = this.context; context.clearRect(0, 0, WIDTH, HEIGHT); // show on high zoom levels only and avoid rendering during zoom if (ZOOM < MIN_ZOOM || isZooming) { return; } var item, h, mh, sortCam = {x: CAM_X + ORIGIN_X, y: CAM_Y + ORIGIN_Y}, footprint, color, dataItems = Data.items; dataItems.sort(function (a, b) { return (a.minHeight - b.minHeight) || getDistance(b.center, sortCam) - getDistance(a.center, sortCam) || (b.height - a.height); }); for (var i = 0, il = dataItems.length; i < il; i++) { item = dataItems[i]; if (!(color = item.hitColor)) { continue; } footprint = item.footprint; if (!isVisible(footprint)) { continue; } h = item.height; mh = 0; if (item.minHeight) { mh = item.minHeight; } switch (item.shape) { case 'cylinder': Cylinder.hitArea(context, item.center, item.radius, item.radius, h, mh, color); break; case 'cone': Cylinder.hitArea(context, item.center, item.radius, 0, h, mh, color); break; case 'dome': Cylinder.hitArea(context, item.center, item.radius, item.radius / 2, h, mh, color); break; case 'sphere': Cylinder.hitArea(context, item.center, item.radius, item.radius, h, mh, color); break; case 'pyramid': Pyramid.hitArea(context, footprint, item.center, h, mh, color); break; default: Block.hitArea(context, footprint, item.holes, h, mh, color); } switch (item.roofShape) { case 'cone': Cylinder.hitArea(context, item.center, item.radius, 0, h + item.roofHeight, h, color); break; case 'dome': Cylinder.hitArea(context, item.center, item.radius, item.radius / 2, h + item.roofHeight, h, color); break; case 'pyramid': Pyramid.hitArea(context, footprint, item.center, h + item.roofHeight, h, color); break; } } this._imageData = this.context.getImageData(0, 0, WIDTH, HEIGHT).data; }, getIdFromXY: function (x, y) { var imageData = this._imageData; if (!imageData) { return; } var pos = 4 * ((y | 0) * WIDTH + (x | 0)); var index = imageData[pos] | (imageData[pos + 1] << 8) | (imageData[pos + 2] << 16); return this._idMapping[index]; }, idToColor: function (id) { var index = this._idMapping.indexOf(id); if (index === -1) { this._idMapping.push(id); index = this._idMapping.length - 1; } var r = index & 0xff; var g = (index >> 8) & 0xff; var b = (index >> 16) & 0xff; return 'rgb(' + [r, g, b].join(',') + ')'; } }; //****** file: Debug.js ****** var Debug = { point: function (x, y, color, size) { var context = this.context; context.fillStyle = color || '#ffcc00'; context.beginPath(); context.arc(x, y, size || 3, 0, 2 * PI); context.closePath(); context.fill(); }, line: function (ax, ay, bx, by, color) { var context = this.context; context.strokeStyle = color || '#ffcc00'; context.beginPath(); context.moveTo(ax, ay); context.lineTo(bx, by); context.closePath(); context.stroke(); } }; //****** file: Layers.js ****** var animTimer; function fadeIn() { if (animTimer) { return; } animTimer = setInterval(function () { var dataItems = Data.items, isNeeded = false; for (var i = 0, il = dataItems.length; i < il; i++) { if (dataItems[i].scale < 1) { dataItems[i].scale += 0.5 * 0.2; // amount*easing if (dataItems[i].scale > 1) { dataItems[i].scale = 1; } isNeeded = true; } } Layers.render(); if (!isNeeded) { clearInterval(animTimer); animTimer = null; } }, 33); } var Layers = { container: document.createElement('DIV'), items: [], init: function () { this.container.style.pointerEvents = 'none'; this.container.style.position = 'absolute'; this.container.style.left = 0; this.container.style.top = 0; // TODO: improve this to .setContext(context) Shadows.context = this.createContext(this.container); Simplified.context = this.createContext(this.container); Buildings.context = this.createContext(this.container); HitAreas.context = this.createContext(); // Debug.context = this.createContext(this.container); }, render: function (quick) { requestAnimFrame(function () { if (!quick) { Shadows.render(); Simplified.render(); HitAreas.render(); } Buildings.render(); }); }, createContext: function (container) { var canvas = document.createElement('CANVAS'); canvas.style.transform = 'translate3d(0, 0, 0)'; // turn on hw acceleration canvas.style.imageRendering = 'optimizeSpeed'; canvas.style.position = 'absolute'; canvas.style.left = 0; canvas.style.top = 0; var context = canvas.getContext('2d'); context.lineCap = 'round'; context.lineJoin = 'round'; context.lineWidth = 1; context.imageSmoothingEnabled = false; this.items.push(canvas); if (container) { container.appendChild(canvas); } return context; }, appendTo: function (parentNode) { parentNode.appendChild(this.container); }, remove: function () { this.container.parentNode.removeChild(this.container); }, setSize: function (width, height) { for (var i = 0, il = this.items.length; i < il; i++) { this.items[i].width = width; this.items[i].height = height; } }, // usually called after move: container jumps by move delta, cam is reset setPosition: function (x, y) { this.container.style.left = x + 'px'; this.container.style.top = y + 'px'; } }; Layers.init(); //****** file: adapter.js ****** function setOrigin(origin) { ORIGIN_X = origin.x; ORIGIN_Y = origin.y; } function moveCam(offset) { CAM_X = CENTER_X + offset.x; CAM_Y = HEIGHT + offset.y; Layers.render(true); } function setSize(size) { WIDTH = size.width; HEIGHT = size.height; CENTER_X = WIDTH / 2 << 0; CENTER_Y = HEIGHT / 2 << 0; CAM_X = CENTER_X; CAM_Y = HEIGHT; Layers.setSize(WIDTH, HEIGHT); MAX_HEIGHT = CAM_Z - 50; } function setZoom(z) { ZOOM = z; if (isLatLngProjection()) { MAP_SIZE = 360 / mapObj.resolution; } else { MAP_SIZE = MAP_TILE_SIZE << ZOOM; } var center = pixelToGeo(ORIGIN_X + CENTER_X, ORIGIN_Y + CENTER_Y); var a = geoToPixel(center.latitude, 0); var b = geoToPixel(center.latitude, 1); PIXEL_PER_DEG = b.x - a.x; ZOOM_FACTOR = pow(0.95, ZOOM - MIN_ZOOM); WALL_COLOR_STR = '' + WALL_COLOR.alpha(ZOOM_FACTOR); ALT_COLOR_STR = '' + ALT_COLOR.alpha(ZOOM_FACTOR); ROOF_COLOR_STR = '' + ROOF_COLOR.alpha(ZOOM_FACTOR); } function onResize(e) { setSize(e); Layers.render(); Data.update(); } function onMoveEnd(e) { Layers.render(); Data.update(); // => fadeIn() => Layers.render() } function onZoomStart() { isZooming = true; // effectively clears because of isZooming flag // TODO: introduce explicit clear() Layers.render(); } function onZoomEnd(e) { isZooming = false; setZoom(e.zoom); Data.update(); // => fadeIn() Layers.render(); } //****** file: SuperMap.js ****** // based on a pull request from Jérémy Judéaux (https://github.com/Volune) var parent = SuperMap.Layer.prototype; var transformExtent = function (extent) { if (isLatLngProjection()) { return { left: -180, bottom: -90, right: 180, top: 90 } } return extent; }; var osmb = function (map) { this.offset = {x: 0, y: 0}; // cumulative cam offset during moveBy() bindMap(map); parent.initialize.call(this, this.name, {projection: 'EPSG:900913'}); if (map) { map.addLayer(this); } }; //bind map to global function bindMap(map) { mapObj = map; } var proto = osmb.prototype = new SuperMap.Layer(); proto.name = 'OSM Buildings'; proto.attribution = ATTRIBUTION; proto.isBaseLayer = false; proto.alwaysInRange = true; proto.addTo = function (map) { this.setMap(map); return this; }; proto.setOrigin = function () { var map = this.map, origin = map.getLonLatFromPixel(new SuperMap.Pixel(0, 0)), res = map.resolution; var ext = transformExtent(this.maxExtent); var x = (origin.lon - ext.left) / res << 0, y = (ext.top - origin.lat) / res << 0; setOrigin({x: x, y: y}); }; proto.setMap = function (map) { if (!this.map) { parent.setMap.call(this, map); } Layers.appendTo(this.div); setSize({width: map.size.w, height: map.size.h}); setZoom(map.zoom); this.setOrigin(); var layerProjection = this.projection; map.events.register('click', map, function (e) { var id = HitAreas.getIdFromXY(e.xy.x, e.xy.y); if (id) { var geo = map.getLonLatFromPixel(e.xy).transform(layerProjection, this.projection); onClick({feature: id, lat: geo.lat, lon: geo.lon}); } }); Data.update(); }; proto.removeMap = function (map) { Layers.remove(); parent.removeMap.call(this, map); this.map = null; }; proto.onMapResize = function () { var map = this.map; parent.onMapResize.call(this); onResize({width: map.size.w, height: map.size.h}); }; proto.moveTo = function (bounds, zoomChanged, isDragging) { var map = this.map, res = parent.moveTo.call(this, bounds, zoomChanged, isDragging); if (!isDragging) { var offsetLeft = parseInt(map.layerContainerDiv.style.left, 10), offsetTop = parseInt(map.layerContainerDiv.style.top, 10); this.div.style.left = -offsetLeft + 'px'; this.div.style.top = -offsetTop + 'px'; } this.setOrigin(); this.offset.x = 0; this.offset.y = 0; moveCam(this.offset); if (zoomChanged) { onZoomEnd({zoom: map.zoom}); } else { onMoveEnd(); } return res; }; proto.moveByPx = function (dx, dy) { this.offset.x += dx; this.offset.y += dy; var res = parent.moveByPx.call(this, dx, dy); moveCam(this.offset); return res; }; //****** file: public.js ****** proto.style = function (style) { style = style || {}; var color; if ((color = style.color || style.wallColor)) { WALL_COLOR = Color.parse(color); WALL_COLOR_STR = '' + WALL_COLOR.alpha(ZOOM_FACTOR); ALT_COLOR = WALL_COLOR.lightness(0.8); ALT_COLOR_STR = '' + ALT_COLOR.alpha(ZOOM_FACTOR); ROOF_COLOR = WALL_COLOR.lightness(1.2); ROOF_COLOR_STR = '' + ROOF_COLOR.alpha(ZOOM_FACTOR); } if (style.roofColor) { ROOF_COLOR = Color.parse(style.roofColor); ROOF_COLOR_STR = '' + ROOF_COLOR.alpha(ZOOM_FACTOR); } if (style.shadows !== undefined) { Shadows.enabled = !!style.shadows; } Layers.render(); return this; }; proto.date = function (date) { Shadows.date = date; Shadows.render(); return this; }; proto.load = function (url) { Data.load(url); return this; }; proto.set = function (data) { Data.set(data); return this; }; var onEach = function () { }; proto.each = function (handler) { onEach = function (payload) { return handler(payload); }; return this; }; var onClick = function () { }; proto.click = function (handler) { onClick = function (payload) { return handler(payload); }; return this; }; osmb.VERSION = VERSION; osmb.ATTRIBUTION = ATTRIBUTION; //****** file: suffix.js ****** global.OSMBuildings = osmb; }(this));