beforefeatureadded event equivalent for OpenLayers 3 - javascript

For openlayers 2, you had an event called beforefeatureadded that you could do validation before actually adding a new feature. What is the equivalent of beforefeatureadded event for OpenLayers 3?
OpenLayers 2 example:
layer.events.register("beforefeatureadded", layer, validationFunction);

The equivalent to the OpenLayers 2 beforefeatureadded event is the use of a staging collection for drawn features:
var source = new ol.source.Vector();
var features = new ol.Collection();
features.on('add', function(evt) {
var feature = evt.element;
if (conditionMet(feature)) {
source.addFeature(evt.element);
}
// clear the staging collection
features.pop();
});
What's also possible is the use of a Draw condition, and this is what #robert-smith actually wants here:
var draw = new ol.interaction.Draw({
condition: function(evt) {
return ol.events.condition.noModifierKeys(evt) && conditionMet(evt);
}
});

Related

OpenLayers overlapping LineStrings issue

I have one set of coordinates from which I create 2 LineStrings
// global variables, I have previously defined map
let route = null;
let routeFeature = null;
let routeOverlay = null;
let routeOverlayFeatur = null;
My main LineString
function createMainRoute(coordinates) {
route = new LineString(coordinates);
routeFeature = new Feature({ geometry: route });
routeFeature.setStyle(styles.mainRoute);
const routeLayer = new VectorLayer({
source: new VectorSource({
features: [routeFeature],
}),
});
map.addLayer(routeLayer);
}
My second LineString with the exact coordinates
function createMainRouteOverlay(coordinates) {
routeOverlay = new LineString(coordinates);
routeOverlayFeature = new Feature({ geometry: routeOverlay });
routeOverlayFeature.setStyle(styles.routeOverlay);
const routeOverlayLayer = new VectorLayer({
source: new VectorSource({
features: [routeOverlayFeature],
}),
});
map.addLayer(routeOverlayLayer );
}
Now I have a function that draws these 2 lines, this function is called when I get initial data from server
function init(coordinates) {
createMainRoute(coordinates);
createMainRouteOverlay(coordinates);
}
Goal is that I want to modify coordinates on the overlay line like the coordinates, style and so on without loosing track of the main route, but they are at the start the same.
Now I have a function that will dynamically change coordinates set of the overlay line. This function is called when the external data is changed.
function readjust(newCoordinates) {
routeOverlayFeature.getGeomety().setCoordinates(newCoordinates)
}
The problem here is that when I call readjust(), nothing happens on the map, line stays the same with these new coordinates, but when I comment out createMainRoute() from the init function, meaning i only have one line and just call readjust(), line is updated with the new data. Does anyone knows what is the issue here? Is something like this not supported or do I need to set some property to allow this? I don't get why they are in conflict when they are 2 separated variables and only one is updated. Any help is appreciated
Your features are sharing the same geometry
routeOverlayFeature = new Feature({ geometry: route });
should be
routeOverlayFeature = new Feature({ geometry: routeOverlay });

Showing an infoWindow immediately after a Layer query succeeded

I have a web application, where a user can switch between some 160-ish layers. Most of them are Feature Layers, but some are of type ArcGISDynamicMapServiceLayer.
I need to be able to query those layers the same as I do with FeatureLayers: by clicking on any point on the map and displaying an infowindow.
This is my code so far (removed some bits for clarity):
executeQueryTask: function(evt, scope) {
//"this" is the map object in this context, so we pass in the scope from the caller,
//which will enable us to call the layer and map object and all the other precious widget properties
scope.map.graphics.clear();
scope.map.infoWindow.hide();
//we create a new Circle and set its center at the mappoint. The radius will be 20 meters
//default unit is meters.
var circle = new Circle({
/*...*/
});
// draw the circle to the map:
var circleGraphic = new Graphic(circle, /*...*/));
scope.map.graphics.add(circleGraphic);
var queryTask = new QueryTask(scope.layer.layer.url + "/" + scope.layer.layer.visibleLayers[0]);
var query = new Query();
query.returnGeometry = true;
query.outFields = ["*"];
query.geometry = circle.getExtent();
var infoTemplate = new InfoTemplate().setTitle("");
queryTask.execute(query, function(resultSet) {
array.forEach(resultSet.features, function(feature) {
var graphic = feature;
graphic.setSymbol(/*...*/));
//Set the infoTemplate.
// graphic.setInfoTemplate(infoTemplate);
//Add graphic to the map graphics layer.
scope.map.infoWindow.setContent(graphic.attributes);
scope.map.infoWindow.show(evt.mapPoint, scope.map.getInfoWindowAnchor(evt.screenPoint));
scope.map.graphics.add(graphic);
});
});
},
The key point is insise the queryTask.execute callback. If I uncomment and use graphic.setInfoTemplate(infoTemplate); the result is colored and upon a second click an infoWindow pops up.
There are 2 issues with this approach:
2 clicks are needed
I am unable to click on PolyLines and Points twice, so no infowindow pops up here.
This is why I added a circle to get a 100m buffer in radius to my click. Now I want to immediatly return an infoWindow.
At this point I'm struggeling to successfully create an Info Window, which is immediately displayed.
Currently the line scope.map.infoWindow.setContent(graphic.attributes); throws an error Failed to execute 'appendChild' on 'Node': parameter 1 is not of type 'Node'.
How can I create that Info Window?
I found a suitable approach, which leaves room for improvements. But this is for another iteration.
//create a new FeatureLayer object
var featureLayer = new FeatureLayer(scope.layer.layer.url + "/" + scope.layer.layer.visibleLayers[0], {
mode: FeatureLayer.MODE_SELECTION,
infoTemplate: new InfoTemplate("Attributes", "${*}"),
outFields: ["*"]
});
//we create a new Circle and set its center at the mappoint. The radius will be 20 meters
//default unit is meters.
var circle = new Circle({/*...*/});
// draw the circle to the map:
var circleGraphic = new Graphic(circle, /*...*/));
scope.map.graphics.add(circleGraphic);
var lQuery = new Query();
lQuery.returnGeometry = true;
lQuery.geometry = circle.getExtent();
featureLayer.queryFeatures(lQuery, function(results) {
array.forEach(results.features, function(feature) {
var graphic = feature;
graphic.setSymbol(/*...*/));
//now that we have the feature, we need to select it
var selectionQuery = new Query();
selectionQuery.geometry = feature.geometry;
featureLayer.selectFeatures(selectionQuery, FeatureLayer.SELECTION_NEW)
.then(function(selectedFeatures) {
console.info("selection complete", selectedFeatures);
if (!selectedFeatures.length) {
return;
}
scope.map.infoWindow.setFeatures(selectedFeatures);
scope.map.infoWindow.show(evt.mapPoint, "upperright");
});
});
});
The change here is, that we are no longer using a QueryTask, but create a new FeatureLayer object in selection mode, using the url and id of the visible layer.
The second noteworthy change is, that we no longer set the content of the infoWindow, but instead set selected features using infoWindow.setFeatures(selectedFeatures). Setting the content of an infoWindow, but not selecting features, hides the action list of the info window, this hinders you to zoom to an object or perform other custom operations.
In addition, this enables you( or me ) to view multiple results in the infoWindow, instead of just one.

Confused with Viewer API vs example, passing options to extensions

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.

OpenLayers destroyFeatures() doesn't destroy features when using a clustering strategy

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([]);

Google Maps v3 - Delete vertex on Polygon

Google Maps has the Drawing library to draw Polylines and Polygons and other things.
Example of this functionality here: http://gmaps-samples-v3.googlecode.com/svn-history/r282/trunk/drawing/drawing-tools.html
I want, when drawing and editing the polygon, to be able to delete one point/vertex on the path. The API docs haven't seemed to hint at anything.
Google Maps now provides a "PolyMouseEvent" callback object on events that are triggered from a Polygon or Polyline.
To build on the other answers which suggested a solution involving a right click, all you would need to do is the following in the latest versions of the V3 API:
// this assumes `my_poly` is an normal google.maps.Polygon or Polyline
var deleteNode = function(mev) {
if (mev.vertex != null) {
my_poly.getPath().removeAt(mev.vertex);
}
}
google.maps.event.addListener(my_poly, 'rightclick', deleteNode);
You'll notice that any complex calculations on whether or not we are near the point are no longer necesary, as the Google Maps API is now telling us which vertex we've clicked on.
Note: this will only work while the Polyline/Polygon is in edit mode. (Which is when the vertices you might want to delete are visible.)
As a final thought, you could consider using a click or double click event instead. "Click" is smart enough to not trigger on a drag, though using a single click trigger might still surprise some of your users.
This is currently an outstanding feature request (acknowledged by Google), issue 3760.
Here's my solution: http://jsbin.com/ajimur/10. It uses a function that adds a delete button to the passed in polygon (below the undo button).
Alternatively, someone suggested this approach: right-click to delete closest vertex, which works fine but is somewhat lacking in UI finesse. I built on the code from the link to check if the click was inside (or within 1 pixel of) the node - in a JSBin here: http://jsbin.com/ajimur/.
EDIT: as Amr Bekhit pointed out - this approach is currently broken, as the events need to be attached to the polygon.
I found Sean's code very simple and helpful. I just added a limiter to stop deleting when the user has only 3 nodes left. Without it, the user can get down to just one node, and can't edit anymore:
my_poly.addListener('rightclick', function(mev){
if (mev.vertex != null && this.getPath().getLength() > 3) {
this.getPath().removeAt(mev.vertex);
}
});
I ran into situations where I needed to delete nodes from polygons that contained multiple paths. Here's a modification of Sean's and Evil's code:
shape.addListener('rightclick', function(event){
if(event.path != null && event.vertex != null){
var path = this.getPaths().getAt(event.path);
if(path.getLength() > 3){
path.removeAt(event.vertex);
}
}
});
Just thought I'd contribute because I was looking for a solution for this too, here's my implementation:
if (m_event.hasOwnProperty('edge') && m_event.edge >= 0 &&
GeofenceService.polygon.getPath().getLength() > 3) {
GeofenceService.polygon.getPath().removeAt(m_event.edge);
return;
}
if (m_event.hasOwnProperty('vertex') && m_event.vertex >= 0 &&
GeofenceService.polygon.getPath().getLength() > 3) {
GeofenceService.polygon.getPath().removeAt(m_event.vertex);
return;
}
This allows for handling deletion of vertex nodes AND edge nodes, and maintains a minimum of a triangle formation polygon at all times by checking the path length > 3.
2020 Update
Google provides a working demo of this in their documentation which demonstrates how a to delete a vertex, or a point on the line, by right-clicking on a vertex to show a "Delete" menu.
Check out Deleting a Vertex
And the code for completeness (see their Github repo);
function initialize() {
const mapOptions = {
zoom: 3,
center: new google.maps.LatLng(0, -180),
mapTypeId: "terrain",
};
const map = new google.maps.Map(document.getElementById("map"), mapOptions);
const flightPlanCoordinates = [
new google.maps.LatLng(37.772323, -122.214897),
new google.maps.LatLng(21.291982, -157.821856),
new google.maps.LatLng(-18.142599, 178.431),
new google.maps.LatLng(-27.46758, 153.027892),
];
const flightPath = new google.maps.Polyline({
path: flightPlanCoordinates,
editable: true,
strokeColor: "#FF0000",
strokeOpacity: 1.0,
strokeWeight: 2,
map: map,
});
/**
* A menu that lets a user delete a selected vertex of a path.
*/
class DeleteMenu extends google.maps.OverlayView {
constructor() {
super();
this.div_ = document.createElement("div");
this.div_.className = "delete-menu";
this.div_.innerHTML = "Delete";
const menu = this;
google.maps.event.addDomListener(this.div_, "click", () => {
menu.removeVertex();
});
}
onAdd() {
const deleteMenu = this;
const map = this.getMap();
this.getPanes().floatPane.appendChild(this.div_);
// mousedown anywhere on the map except on the menu div will close the
// menu.
this.divListener_ = google.maps.event.addDomListener(
map.getDiv(),
"mousedown",
(e) => {
if (e.target != deleteMenu.div_) {
deleteMenu.close();
}
},
true
);
}
onRemove() {
if (this.divListener_) {
google.maps.event.removeListener(this.divListener_);
}
this.div_.parentNode.removeChild(this.div_);
// clean up
this.set("position", null);
this.set("path", null);
this.set("vertex", null);
}
close() {
this.setMap(null);
}
draw() {
const position = this.get("position");
const projection = this.getProjection();
if (!position || !projection) {
return;
}
const point = projection.fromLatLngToDivPixel(position);
this.div_.style.top = point.y + "px";
this.div_.style.left = point.x + "px";
}
/**
* Opens the menu at a vertex of a given path.
*/
open(map, path, vertex) {
this.set("position", path.getAt(vertex));
this.set("path", path);
this.set("vertex", vertex);
this.setMap(map);
this.draw();
}
/**
* Deletes the vertex from the path.
*/
removeVertex() {
const path = this.get("path");
const vertex = this.get("vertex");
if (!path || vertex == undefined) {
this.close();
return;
}
path.removeAt(vertex);
this.close();
}
}
const deleteMenu = new DeleteMenu();
google.maps.event.addListener(flightPath, "rightclick", (e) => {
// Check if click was on a vertex control point
if (e.vertex == undefined) {
return;
}
deleteMenu.open(map, flightPath.getPath(), e.vertex);
});
}

Categories

Resources