I am confused as the examples on how to use the Viewer don't seem to match the documentation of the API, some functions are not in the docs or their signature is different.
Base on the examples code, how do I pass options to the extensions I instantiate? I would like to pass my extension a callback.
Thanks!
We need to fix the doc so it does not rely anymore on the undocumented A360 viewer additional code, which is supposed to be internal. Sorry for the incovenience, we will do this asap...
For the time being, you can use the code from my viewer boilerplate sample:
function initializeViewer(containerId, urn) {
Autodesk.Viewing.Document.load(urn, function (model) {
var rootItem = model.getRootItem();
// Grab all 3D items
var geometryItems3d = Autodesk.Viewing.Document.getSubItemsWithProperties(
rootItem,
{ 'type': 'geometry', 'role': '3d' },
true);
// Grab all 2D items
var geometryItems2d = Autodesk.Viewing.Document.getSubItemsWithProperties(
rootItem,
{ 'type': 'geometry', 'role': '2d' },
true);
var domContainer = document.getElementById(containerId);
//UI-less Version: viewer without any Autodesk buttons and commands
//viewer = new Autodesk.Viewing.Viewer3D(domContainer);
//GUI Version: viewer with controls
viewer = new Autodesk.Viewing.Private.GuiViewer3D(domContainer);
viewer.initialize();
viewer.setLightPreset(8);
//Button events - two buttons to load/unload a sample extension
// Irrelevant to viewer code itself
var loadBtn = document.getElementById('loadBtn');
loadBtn.addEventListener("click", function(){
loadExtension(viewer);
});
var unloadBtn = document.getElementById('unloadBtn');
unloadBtn.addEventListener("click", function(){
unloadExtension(viewer);
});
// Illustrates how to listen to events
// Geometry loaded is fired once the model is fully loaded
// It is safe to perform operation involving model structure at this point
viewer.addEventListener(
Autodesk.Viewing.GEOMETRY_LOADED_EVENT,
onGeometryLoaded);
//optional
var options = {
globalOffset: {
x: 0, y: 0, z: 0
}
}
// Pick the first 3D item ortherwise first 2D item
var viewablePath = (geometryItems3d.length ?
geometryItems3d[0] :
geometryItems2d[0]);
viewer.loadModel(
model.getViewablePath(viewablePath),
options);
}, function(err) {
logError(err);
});
}
Once the viewer is initialized, you can load independently each extension and pass a callback as follow:
var options = {
onCustomEventFiredByMyExtension: function() {
console.log('LMV rulez!')
}
}
viewer.loadExtension('MyExtensionId', options)
But I think a more elegant approach would be to fire events from the extension itself, which may look like this:
viewer.loadExtension('MyExtensionId')
var myExtension = viewer.getExtension('MyExtensionId')
myExtension.on('CustomEvent', function () {
console.log('LMV still rulez!')
})
See micro-events for a super simple event library.
Related
I am trying to implement a in browser raster drawing plugin for the leaflet library that that extends the leaflets GridLayer api. Essentially for every tile there is function createTile that returns a canvas with some drawing on it. and leaflet shows the tile in correct position.
initialize: function(raster_data){
this.raster_data = raster_data;
},
createTile: function (tile_coords) {
let _tile = document.createElement('canvas');
let _tile_ctx = _tile.getContext('2d');
// do some drawing here with values from this.raster_data
return _tile;
}
This implementation is so far working fine. Than I thought of offloading drawing with offscreen-canvas in a webworker. so I restructured the code like this
initialize: function(raster_data){
this.raster_data = raster_data;
this.tile_worker = new Worker('tile_renderer.js')
},
createTile: function (tile_coords) {
let _tile = document.createElement('canvas').transferControlToOffscreen();
this.tile_worker.postMessage({
_tile: _tile,
_raster_data: this.raster_data
},[_tile])
return _tile;
}
This works but every now and then i see a canvas that is just blank. That thing is quite random I don't know start from where and how should I debug this. can this be a problem that I am using a single worker for rendering every tile? any help is appreciated. Here is an example of a blank canvas.
This a known bug: https://crbug.com/1202481
The issue appears when too many OffscreenCanvases are sent to the Worker serially.
The workaround is then to batch send all these OffscreenCanvases in a single call to postMessage().
In your code you could achieve this by storing all the objects to be sent and use a simple debouncing strategy using a 0 timeout to send them all at once:
createTile: function (tile_coords) {
let _tile = document.createElement('canvas');
_tile.setAttribute('width', 512);
_tile.setAttribute('height', 512);
let _off_tile = _tile.transferControlToOffscreen();
this.tiles_to_add.push( _off_tile ); // store for later
clearTimeout( this.batch_timeout_id ); // so that the callback is called only once
this.batch_timeout_id = setTimeout( () => {
// send them all at once
this.tileWorker.postMessage( { tiles: this.tiles_to_add }, this.tiles_to_add );
this.tiles_to_add.length = 0;
});
return _tile;
}
Live example: https://artistic-quill-tote.glitch.me/
Is there any way to calculate the extent of a KML layer loaded from the web using the KMLLayer({ url: "my file" }) method in ArcGIS Online? The KMLs loaded from AGOL have a valid fullExtent property, but ones loaded from other sources seem to default to the entire world, which is not useful.
Here is an example:
app.kml=new KMLLayer({ url: "my file" });
app.map.add(app.kml);
app.kml.load().then(function() { app.mapView.extent=app.kml.fullExtent; console.log(app.kml) });
It is live at:
http://viseyes.org/visualeyes/test.htm?kml=https://www.arcgis.com/sharing/rest/content/items/a8efe6f4c12b462ebedc550de8c73e22/data
The console prints out the KMLLayer object, and the fullExtent field seems to be not set right.
I agree, it does not seem like the fullExtent property is what you would expect. I think there are two workarounds:
Write some code to query the layerView to get the extent:
view.whenLayerView(kmlLayer).then(function(layerView) {
watchUtils.whenFalseOnce(layerView, "updating", function() {
var kmlFullExtent = queryExtent(layerView);
view.goTo(kmlFullExtent);
});
});
function queryExtent(layerView) {
var polygons = layerView.allVisiblePolygons;
var lines = layerView.allVisiblePolylines;
var points = layerView.allVisiblePoints;
var images = layerView.allVisibleMapImages;
var kmlFullExtent = polygons
.concat(lines)
.concat(points)
.concat(images)
.map(
graphic => (graphic.extent ? graphic.extent : graphic.geometry.extent)
)
.reduce((previous, current) => previous.union(current));
return kmlFullExtent;
}
Example here.
-- or --
Call the utility service again and use the "lookAtExtent" property:
view.whenLayerView(kmlLayer).then(function(layerView) {
watchUtils.whenFalseOnce(layerView, "updating", function() {
// Query the arcgis utility and use the "lookAtExtent" property -
esriRequest('https://utility.arcgis.com/sharing/kml?url=' + kmlLayer.url).then((response) => {
console.log('response', response.data.lookAtExtent);
view.goTo(new Extent(response.data.lookAtExtent));
});
});
});
Example here.
I'm new to Backbone.js, and someone who comes out of the 'standard' model of JS development I'm a little unsure of how to work with the models (or when).
Views seem pretty obvious as it emulates the typical 'listen for event and do something' method that most JS dev's are familiar with.
I built a simple Todo list app and so far haven't seen a need for the model aspect so I'm curious if someone can give me some insight as to how I might apply it to this application, or if it's something that comes into play if I were working with more complex data.
Here's the JS:
Todos = (function(){
var TodoModel = Backbone.Model.extend({
defaults: {
content: null
}
});
var TodoView = Backbone.View.extend({
el: $('#todos'),
newitem: $('#new-item input'),
noitems: $('#no-items'),
initialize: function(){
this.el = $(this.el);
},
events: {
'submit #new-item': 'addItem',
'click .remove-item': 'removeItem'
},
template: $('#item-template').html(),
addItem: function(e) {
e.preventDefault();
this.noitems.remove();
var templ = _.template(this.template);
this.el.append(templ({content: this.newitem.val()}));
this.newitem.val('').focus();
return this;
},
removeItem: function(e){
$(e.target).parent('.item-wrap').remove();
}
});
self = {};
self.start = function(){
new TodoView();
};
return self;
});
$(function(){
new Todos(jQuery).start();
});
Which is running here: http://sandbox.fluidbyte.org/bb-todo
Model and Collection are needed when you have to persist the changes to the server.
Example:
var todo = new TodoModel();
creates a new model. When you have to save the save the changes, call
todo.save();
You can also pass success and error callbacks to save . Save is a wrapper around the ajax function provided by jQuery.
How to use a model in your app.
Add a url field to your model
var TodoModel = Backbone.Model.extend({
defaults: {
content: null
},
url: {
"http://localhost";
}
});
Create model and save it.
addItem: function(e) {
e.preventDefault();
this.noitems.remove();
var templ = _.template(this.template);
this.el.append(templ({content: this.newitem.val()}));
this.newitem.val('').focus();
var todo = new TodoModel({'content':this.newitem.val()});
todo.save();
return this;
},
Make sure your server is running and set the url is set correctly.
Learning Resources:
Check out the annotated source code of Backbone for an excellent
explanation of how things fall into place behind the scenes.
This Quora question has links to many good resources and sample apps.
The model is going to be useful if you ever want to save anything on the server side. Backbone's model is built around a RESTful endpoint. So if for example you set URL root to lists and then store the list information in the model, the model save and fetch methods will let you save/receive JSON describing the mode to/from the server at the lists/<id> endpoint. IE:
ToDoListModel = Backbone.model.extend( {
urlRoot : "lists/" } );
// Once saved, lives at lists/5
list = new ToDoListModel({id: 5, list: ["Take out trash", "Feed Dog"] });
list.save();
So you can use this to interact with data that persists on the server via a RESTful interface. see this tutorial for more.
I disagree with the idea that model is needed only to persist changes (and I am including LocalStorage here, not only the server).
It is nice to have representation of models and collections so that you have object to work with and not only Views. In your example you are only adding and removing divs (html) from the page, which is something you can do normally with jQuery. Having a Model created and added to a Collection everytime you do "add" and maybe removed when you clear it will allow you some nice things, like for example sorting (alphabetically), or filtering (if you want to implement the concept of "complete" to-do).
In your code, for example:
var TodoModel = Backbone.Model.extend({
defaults: {
content: null
complete: false
}
});
var Todos = Backbone.Collection.extend({
model: TodoModel
})
In the View (irrelevant code is skipped):
// snip....
addItem: function(e) {
e.preventDefault();
this.noitems.remove();
var templ = _.template(this.template);
var newTodo = new TodoModel({ content: this.newitem.val() });
this.collection.add(newTodo); // you get the collection property from free from the initializer in Backbone
this.el.append(templ({model: newTodo})); // change the template here of course to use model
this.newitem.val('').focus();
return this;
},
Initialize like this:
self.start = function(){
new TodoView(new Todos());
};
Now you have a backing Collection and you can do all sort of stuff, like filtering. Let's say you have a button for filtering done todos, you hook this handler:
_filterDone: function (ev) {
filtered = this.collection.where({ complete: true });
this.el.html(''); // empty the collection container, I used "el" but you know where you are rendering your todos
_.each(filtered, function (todo) {
this.el.append(templ({model: todo})); // it's as easy as that! :)
});
}
Beware that emptying the container is probably not the best thing if you have events hooked to the inner views but as a starter this works ok.
You may need a hook for setting a todo done. Create a button or checkbox and maybe a function like this:
_setDone: function (ev) {
// you will need to scope properly or "this" here will refer to the element clicked!
todo = this.collection.get($(ev.currentTarget).attr('todo_id')); // if you had the accuracy to put the id of the todo somewhere within the template
todo.set('complete', true);
// some code here to re-render the list
// or remove the todo single view and re-render it
// in the simplest for just redrawr everything
this.el.html('');
_.each(this.collection, function (todo) {
this.el.append(templ({model: todo}));
});
}
The code above would not have been so easy without Models and Collections and as you can see it does not relate in any way with the server.
A similar issue to that described in OpenLayers: destroyed features reappear after zooming in or out
Call destroyFeatures() or removeAllFeatures() (or both, in either order) on a vector layer. The features disappear from view. But then zoom in or out, and they reappear. For me, this only happens if a clustering strategy is being used. It almost seems as if the cluster feature is destroyed but the underlying features represented by that cluster are not, so, when you zoom in or out the clustering is recalculated from those underlying features and re-drawn.
Googling reveals a number of conversations among the developers of OpenLayers a few years ago concerning issues with destroyFeatures(). It's surprising that, even now, the issues don't seem to have been fully resolved.
I can get around the problem by destroying the whole layer (using destroy()) and then recreating it when needed. That's OK in my case, but I can imagine cases where such a blunderbuss approach might not be desirable.
In response to the request for a code sample, here is an abbreviated version of the code in the version that is working (ie, using destroy()). In the non-working version, I called destroyFeatures() instead (and did not set the layer to null). As stated above, this would erase the features initially, but if I then zoomed in or out using this.map.zoomIn(), the features would reappear.
Note 1: the functions are called from Objective-C via the JavaScript bridge.
Note 2: the JavaScript was generated using CoffeeScript.
(function() {
var addSightingsLayer, displayFeaturesForSightingsWithGeoJSONData, geoJSONFormat, load, map, projectionSphericalMercator, projectionWGS84, removeSightingsLayer, sightingsLayer;
addSightingsLayer = function() {
var context, layerStyle, layerStyleSelected, style, styleMap, styleSelected, yerClusteringStrategy;
if (!(this.sightingsLayer === null)) return;
yerClusteringStrategy = new OpenLayers.Strategy.Cluster({
distance: 10,
threshold: 2
});
this.sightingsLayer = new OpenLayers.Layer.Vector('Sightings', {
strategies: [yerClusteringStrategy]
});
this.map.addLayer(this.sightingsLayer);
style = {
// Here I define a style
};
context = {
// Here I define a context, with several functions depending on whether there is a cluster or not, eg:
dependentLabel: function(feature) {
if (feature.cluster) {
return feature.attributes.count;
} else {
return feature.attributes.name;
}
}, ....
};
layerStyle = new OpenLayers.Style(style, {
context: context
});
styleSelected = {
//...
};
layerStyleSelected = new OpenLayers.Style(styleSelected, {
context: context
});
styleMap = new OpenLayers.StyleMap({
'default': layerStyle,
'select': layerStyleSelected
});
this.sightingsLayer.styleMap = styleMap;
};
removeSightingsLayer = function() {
if (this.sightingsLayer === null) return;
this.sightingsLayer.destroy();
return this.sightingsLayer = null;
};
displayFeaturesForSightingsWithGeoJSONData = function(geoJSONData) {
if (this.sightingsLayer === null) JFOLMap.addSightingsLayer();
return this.sightingsLayer.addFeatures(this.geoJSONFormat.read(geoJSONData));
};
load = function() {
var lat, lon, osmLayer, zoom;
lat = ...;
lon = ...;
zoom = ...;
this.map = new OpenLayers.Map('mapDiv', {
controls: ...,
eventListeners: ...
});
osmLayer = new OpenLayers.Layer.OSM();
this.map.addLayer(osmLayer);
return this.map.setCenter(new OpenLayers.LonLat(lon, lat).transformWGS84ToSphericalMercator(), zoom);
};
OpenLayers.LonLat.prototype.transformWGS84ToSphericalMercator = function() {
return this.transform(JFOLMap.projectionWGS84, JFOLMap.projectionSphericalMercator);
};
OpenLayers.LonLat.prototype.transformSphericalMercatorToWGS84 = function() {
return this.transform(JFOLMap.projectionSphericalMercator, JFOLMap.projectionWGS84);
};
map = null;
sightingsLayer = null;
sightingsPopoverControl = null;
projectionWGS84 = new OpenLayers.Projection('EPSG:4326');
projectionSphericalMercator = new OpenLayers.Projection('EPSG:900913');
geoJSONFormat = new OpenLayers.Format.GeoJSON({
'externalProjection': projectionWGS84,
'internalProjection': projectionSphericalMercator
});
this.JFOLMap = {
map: map,
sightingsLayer: sightingsLayer,
projectionSphericalMercator: projectionSphericalMercator,
projectionWGS84: projectionWGS84,
geoJSONFormat: geoJSONFormat,
load: load,
addSightingsLayer: addSightingsLayer,
removeSightingsLayer: removeSightingsLayer,
displayFeaturesForSightingsWithGeoJSONData: displayFeaturesForSightingsWithGeoJSONData,
};
}).call(this);
Try this, it worked for me
layer.removeAllFeatures();
layer.destroyFeatures();//optional
layer.addFeatures([]);
I've been reading and hacking around with https://developer.mozilla.org/en/XUL_School/Intercepting_Page_Loads but can seem to do what I need.
I'm working on Chromeless, trying to prevent the main xulbrowser element from ever being navigated away from, e.g., links should not work, neither should window.location.href="http://www.example.com/".
I'm assuming I can do this via browser.webProgress.addProgressListener and then listen to onProgressChange but I can't figure out how to differentiate between a resource request and the browser changing locations (it seems that onLocationChange is too late as the document is already being unloaded).
browser.webProgress.addProgressListener({
onLocationChange: function(){},
onStatusChange: function(){},
onStateChange: function(){},
onSecurityChange: function(){},
onProgressChange: function(){
aRequest.QueryInterface(Components.interfaces.nsIHttpChannel)
if( /* need to check if the object triggering the event is the xulbrowser */ ){
aRequest.cancel(Components.results.NS_BINDING_ABORTED);
}
},
QueryInterface: xpcom.utils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference])
}, wo._browser.webProgress.NOTIFY_ALL);
Another option that sounds promising is the nsIContentPolicy.shouldLoad() method but I really have no clue how to "create an XPCOM component that extends nsIContentPolicy and register it to the "content-policy" category using the nsICategoryManager."
Any Ideas?
I got help on this from the mozilla's #xulrunner irc channel.
Resulting solution follows.
Note: this is a module for use in Mozilla Chromeless, the require("chrome") and require("xpcom") bits will NOT be available under normal circumstances.
const {Cc, Ci, Cu, Cm, Cr} = require("chrome");
const xpcom = require("xpcom");
/***********************************************************
class definition
***********************************************************/
var description = "Chromeless Policy XPCOM Component";
/* UID generated by http://www.famkruithof.net/uuid/uuidgen */
var classID = Components.ID("{2e946f14-72d5-42f3-95b7-4907c676cf2b}");
// I just made this up. Don't know if I'm supposed to do that.
var contractID = "#mozilla.org/chromeless-policy;1";
//class constructor
function ChromelessPolicy() {
//this.wrappedJSObject = this;
}
// class definition
var ChromelessPolicy = {
// properties required for XPCOM registration:
classDescription: description,
classID: classID,
contractID: contractID,
xpcom_categories: ["content-policy"],
// QueryInterface implementation
QueryInterface: xpcom.utils.generateQI([Ci.nsIContentPolicy,
Ci.nsIFactory, Ci.nsISupportsWeakReference]),
// ...component implementation...
shouldLoad : function(aContentType, aContentLocation, aRequestOrigin, aContext, aMimeTypeGuess, aExtra) {
let result = Ci.nsIContentPolicy.ACCEPT;
// only filter DOCUMENTs (not SUB_DOCUMENTs, like iframes)
if( aContentType === Ci.nsIContentPolicy["TYPE_DOCUMENT"]
// block http(s) protocols...
&& /^http(s):/.test(aContentLocation.spec) ){
// make sure we deny the request now
result = Ci.nsIContentPolicy.REJECT_REQUEST;
}
// continue loading...
return result;
},
createInstance: function(outer, iid) {
if (outer)
throw Cr.NS_ERROR_NO_AGGREGATION;
return this.QueryInterface(iid);
}
};
let registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
try
{
Cm.nsIComponentRegistrar.registerFactory(classID, description, contractID, ChromelessPolicy);
}
catch (e) {
// Don't stop on errors - the factory might already be registered
Cu.reportError(e);
}
const categoryManager = Cc["#mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager);
for each (let category in ChromelessPolicy.xpcom_categories) {
categoryManager.addCategoryEntry(category, ChromelessPolicy.classDescription, ChromelessPolicy.contractID, false, true);
}
Pull Request on github for those that are interested: https://github.com/mozilla/chromeless/pull/114