/**
|
* @name Extruded Feature Layer
|
* @author: Nianwei Liu
|
* @fileoverview
|
* A canvas based ESRI ArcGIS Server JavaScript API ported from Open Streem Map Buildings.(osmbuildings.org).
|
*/
|
|
/*(function() {
|
if (!window.OSMBuildings) {
|
// only include minimal filters: invert and desaturate. If need more, include the pixastic.all.js in header before require this.
|
var src = dojo.moduleUrl("agsjs", "osm/OSMBuildings.js");
|
var s = dojo.create("script", {
|
type: "text/javascript",
|
src: src
|
});
|
dojo.doc.getElementsByTagName("head")[0].appendChild(s);
|
}
|
}());
|
*/
|
// ********* end of osm *******************/
|
dojo.provide("agsjs.layers.ExtrudedFeatureLayer");
|
|
dojo.addOnLoad(function() {
|
dojo.declare("agsjs.layers.ExtrudedFeatureLayer", esri.layers.Layer, {
|
_osmb: null,
|
_canvas: null,
|
_tileInfo: null,
|
_mode: 0,// ON_DEMAND|SNAPSHOT
|
_heightAttribute: '',
|
_oidField: null,// will be overridden from meta query
|
_query: null,
|
_task: null,
|
_oids: null, //track current objectids to mark new/old for animation fade in
|
_featureExt: null,//current feature extent, 1.5 map extent
|
_suspendOnPan: false,//whether to suspend drawing during map panning. default is false. set to true if performance is not optimal (non-Chrome browsers);
|
/**
|
* @name ExtrudedFeatureLayerOptions
|
* @class This is an object literal that specify the options for each ExtrudedFeatureLayer.
|
* @property {string} heightAttribute required. name of the attribute for height;
|
* @property {number} defaultHeight optional. default Height to use if the height value is 0. default=0;
|
* @property {number} heightScaleRatio optional. number used to multiple the value from service. default=1;
|
* @property {number} extentScaleRatio optional. extra buffer on map extent to load features to reduce server traffic. default=1.5;
|
* @property {int} [mode] optional. agsjs.layers.ExtrudedFeatureLayer.MODE_ONDEMAND | MODE_SNAPSHOT. default=ON_DEMAND
|
* @property {esri.tasks.Query} [query] optional. query set on the feature layer for retrieving data.
|
* @property {Object} [style] object with color, roofColor (#ffcc00' 'rgb(255,200,200)' 'rgba(255,200,200,0.9)), strokeRoofs (boolean)
|
*/
|
/**
|
* Create an ExtrudedFeatureLayer
|
* @name ExtrudedFeatureLayer
|
* @constructor
|
* @class This class is a ExtrudedFeatureLayer, such as a polygon feature layer with a height attribute.
|
* @param {string||FeatureCollection} url
|
* @param {ExtrudedFeatureLayerOptions} opts
|
*/
|
constructor: function(url, opts) {
|
// Manually call superclass constructor with required arguments
|
if (!(!!document.createElement('canvas').getContext)){
|
throw new Error('Canvas unsupported. Try different browser');
|
}
|
this.inherited(arguments);
|
var opts = opts || {};
|
this._heightAttribute = opts.heightAttribute;
|
this._mode = opts.mode || agsjs.layers.ExtrudedFeatureLayer.MODE_ONDEMAND;
|
this._heightScaleRatio = opts.heightScaleRatio || 1;
|
this._extentScaleRatio = opts.extentScaleRatio || 1.5;
|
this._defaultHeight = opts.defaultHeight || 0;
|
this._style = opts.style;
|
// Deal with feature collection
|
if (dojo.isObject(url) && url.featureSet) {
|
this._mode = agsjs.layers.ExtrudedFeatureLayer.MODE_SNAPSHOT;
|
this._setFeatures(url.featureSet.features);
|
this.loaded = true;
|
this.onLoad(this);
|
} else {
|
this._url = url;
|
// get meta data for layer
|
new esri.request({
|
url: this._url,
|
content: {
|
f: "json"
|
},
|
callbackParamName: "callback"
|
}).then(dojo.hitch(this, this._initLayer));
|
this._query = new esri.tasks.Query();
|
dojo.mixin(this._query, opts.query);
|
dojo.mixin(this._query, {
|
returnGeometry: true,
|
outSpatialReference: {
|
"wkid": 4326
|
}
|
});
|
this._task = new esri.tasks.QueryTask(url);
|
dojo.connect(this._task, 'onComplete', dojo.hitch(this, this._onQueryComplete));
|
dojo.connect(this._task, 'onError', esri.config.defaults.io.errorHandler);
|
}
|
},
|
/**********************
|
* @see http://help.arcgis.com/en/webapi/javascript/arcgis/samples/exp_rasterlayer/javascript/RasterLayer.js
|
* Internal Properties
|
*
|
* _map
|
* _element
|
* _context
|
* _mapWidth
|
* _mapHeight
|
* _connects
|
*
|
**********************/
|
//esri.layers.Layer.method
|
_setMap: function(map, container, ind, lod) {
|
this._map = map;
|
|
var element = dojo.create("div", {
|
width: map.width + "px",
|
height: map.height + "px",
|
style: "position: absolute; left: 0px; top: 0px;"
|
}, container);
|
this._osmb = new OSMBuildings();
|
// allow attribution widget to add copyright text
|
this.suspended = false;
|
this.copyright=OSMBuildings.ATTRIBUTION+","+this.copyrightText;
|
|
this._element = element;
|
this._canvas = this._osmb.createCanvas(element);
|
this._osmb.setSize(map.width, map.height);
|
// calc orgins
|
//9241483,13264618
|
if (map.layerIds.length == 0 || !map.getLayer(map.layerIds[0]).tileInfo) {
|
throw new Error('must have at least one tiled layer added before this layer');
|
}
|
this._tileInfo = map.getLayer(map.layerIds[0]).tileInfo;
|
this._osmb.setZoom(map.getLevel()); // ! assume basemap is tiled.
|
this._setOrigin();
|
|
|
//
|
this._loadData();
|
// Event connections
|
this._connects = [];
|
this._connects.push(dojo.connect(map, "onResize", this, this._onResize));
|
this._connects.push(dojo.connect(map, "onPan", this, this._onPan));
|
this._connects.push(dojo.connect(map, "onExtentChange", this, this._onExtentChange));
|
this._connects.push(dojo.connect(map, "onZoomStart", this, this._onZoomStart));
|
return element;
|
},
|
// esri.layers.Layer.method
|
_unsetMap: function(map, container) {
|
this._osmb && this._osmb.destroyCanvas();
|
this._osmb = null;
|
dojo.forEach(this._connects, dojo.disconnect, dojo);
|
if (this._element) {
|
container.removeChild(this._element);
|
}
|
this._map = null;
|
this._element = null;
|
},
|
_initLayer: function(json) {
|
//dojo.mixin(this, json);
|
this.setMinScale(json.minScale || 0);
|
this.setMaxScale (json.maxScale || 0);
|
this.copyrightText = json.copyrightText;
|
dojo.some(json.fields, function(field, i) {
|
if (field.type == 'esriFieldTypeOID') {
|
this._oidField = field.name;
|
return true;
|
}
|
return false;
|
}, this);
|
this._query.outFields = [this._oidField, this._heightAttribute];
|
this.loaded = true;
|
this.onLoad(this);
|
},
|
_setOrigin: function(dx, dy) {
|
var resolution = this._tileInfo.lods[this._map.getLevel()].resolution; //map.getScale()/12/96/3.28084; //inch_pre_ft/px_per_in/ft_per_mt;
|
var topLeft = this._map.toMap(new esri.geometry.Point(0, 0));
|
var x = Math.round((topLeft.x - this._tileInfo.origin.x) / resolution);
|
var y = Math.round((this._tileInfo.origin.y - topLeft.y) / resolution);
|
this._osmb.setOrigin(x+(dx||0), y+(dy||0));
|
this._osmb.setSize(this._map.width, this._map.height);
|
|
},
|
_onResize: function(extent, width, height) {
|
if (this._osmb) {
|
this._osmb.setSize(width,height );
|
this._osmb.render();
|
};
|
},
|
_onPan: function(extent, delta) {
|
if (this._suspendOnPan){
|
dojo.style(this._canvas, {
|
left: delta.x + "px",
|
top: delta.y + "px"
|
});
|
//this._osmb.setCamOffset(-delta.x, -delta.y);
|
} else {
|
this._setOrigin(-delta.x, -delta.y);
|
this._osmb.render();
|
}
|
},
|
_onExtentChange: function(extent, delta, levelChange, lod) {
|
dojo.style(this._canvas, {
|
left: "0px",
|
top: "0px"
|
});
|
this._setOrigin();
|
this._osmb.setCamOffset(0, 0);
|
if (levelChange) {
|
this._osmb.onZoomEnd({
|
zoom: this._map.getLevel()
|
});
|
|
if (this.isVisibleAtScale(this._map.getScale())) {
|
this._loadData();
|
} else {
|
// clear canvas. Current OSMB does not handle null or {} as no feature.
|
this._osmb.geoJSON({
|
features: []
|
});
|
}
|
} else {
|
this._osmb.onMoveEnd();
|
if (this._featureExt && !this._featureExt.contains(extent)) {
|
this._loadData();
|
};
|
}
|
|
},
|
_onZoomStart: function(extent, zoomFactor, anchor, level) {
|
// actually clear the
|
this._osmb.onZoomStart();
|
},
|
_setFeatures: function(features) {
|
var oids = {};
|
var jfs = [];
|
this._oids = this._oids ||{};
|
for (var i = 0; i < features.length; i++) {
|
var f = features[i];
|
|
var oid = f.attributes[this._oidField];
|
var gj = {
|
"type": "Feature",
|
"geometry": {
|
"type": "Polygon",
|
"coordinates": f.geometry.rings
|
},
|
"properties": {
|
"height": (f.attributes[this._heightAttribute] || this._defaultHeight) * this._heightScaleRatio,
|
'isNew': !this._oids[oid]
|
}
|
}
|
// find out the y coords range for sorting
|
var minY = maxY = f.geometry.rings[0][0][1];
|
for (var j = 0; j < f.geometry.rings.length; j++){
|
for (var k = 0; k < f.geometry.rings[j].length; k++){
|
minY = Math.min(minY,f.geometry.rings[j][k][1] );
|
maxY = Math.max(maxY,f.geometry.rings[j][k][1] );
|
}
|
}
|
gj.minY = minY;
|
gj.maxY = maxY;
|
jfs[i] = gj;
|
oids[oid] = f;
|
}
|
// sort features by height and y coord desc for potential drawing improvement
|
jfs.sort(function (a, b){
|
// if polygon a is completely north of b then put a first.
|
// otherwise put the taller one first.
|
// this ensures north/taller poly draw first
|
if (a.maxY < b.minY){
|
return 1;
|
} else if (a.minY > b.maxY){
|
return -1;
|
} else {
|
return b.properties.height - a.properties.height
|
}
|
});
|
this._oids = oids;
|
this._osmb.geoJSON({
|
"type": "FeatureCollection",
|
"features": jfs
|
});
|
if (this._style) {
|
this._osmb.setStyle(this._style);
|
}
|
},
|
_loadData: function() {
|
if (this._mode == agsjs.layers.ExtrudedFeatureLayer.MODE_SNAPSHOT) {
|
if (this._oids) {
|
return;
|
} else {
|
this._query.geometry = null;
|
this._query.where = this._query.where || '1=1';
|
|
}
|
} else {
|
this._featureExt = this._map.extent.expand(this._extentScaleRatio);
|
this._query.geometry = this._featureExt;
|
}
|
this._task.execute(this._query);
|
},
|
_onQueryComplete: function(featureSet) {
|
this._setFeatures(featureSet.features);
|
}
|
});
|
dojo.mixin(agsjs.layers.ExtrudedFeatureLayer, {
|
MODE_ONDEMAND: 0,
|
MODE_SNAPSHOT: 1
|
});
|
});
|
|