hi-ucs/front/public/leaflet/libs/iclient8c/examples/js/OSMBuildings-SuperMap.js

2449 lines
75 KiB
Vue
Raw Normal View History

2022-06-14 09:32:49 +08:00
/**
* 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 = '&copy; <a href="http://osmbuildings.org">OSM Buildings</a>',
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));