2449 lines
75 KiB
Vue
2449 lines
75 KiB
Vue
|
/**
|
||
|
* 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 = '© <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));
|
||
|
|
||
|
|