i am using the ARCGIS Javascript API and trying to override the default right click behavior of the vertex points of a shape.
in ESRI's help it does list the onVertexClick event however from here it seems there is no way to determine if this is a right or left click event so i cannot override just the rightclick.
https://developers.arcgis.com/javascript/jsapi/edit.html
I am trying to set the right click behavour to just delete the current node/vertex instead of showing a menu with the option Delete.
EDIT
Here is the current event that exists within the ARCGIS api.
this.eventsList.push(dojo.connect(this._editToolbar, 'onVertexClick', $.proxy(this.addCustomVertexClickEvent, this)));
this event is already in the api however it does not return any way for me to determine left/right click.
your comment "listen for the click event then test the button attribute of the MouseEvent object" would work however i cant actually add a click event to the vertex points directly as these are inside the ARCGIS api code.
For anyone else who is looking for a way to do this without hacking around. You can listen to "contextmenu" (right click) events on the body, set a flag in the "contextmenu" handler to let the application know the current state. Simulate a click event to the "vertex handle" with a "mousedown", "mouseup" combination. In the "vertex-click" handler check for the right click flag set in the "contextmenu" handler
var editToolbar = new Edit(map, options);
var rightClick;
$('body').on('contextmenu', function(e) {
var target = e.target;
if(target.tagName === 'circle') {
// We only care about this event if it targeted a vertex
// which is visualized with an SVG circle element
// Set flag for right click
rightClick = true;
// Simulate click on vertex to allow esri vertex-click
// to fill in the data for us
var mouseDownEvt = new MouseEvent('mousedown', e.originalEvent);
target.dispatchEvent(mouseDownEvt);
var mouseUpEvt = new MouseEvent('mouseup', e.originalEvent);
target.dispatchEvent(mouseUpEvt);
// Since this event will be handled by us lets prevent default
// and stop propagation so the browser context menu doesnt appear
e.preventDefault();
e.stopPropagation();
}
});
editToolbar.on('vertex-click', function(e) {
if(rightClick) {
// Handle the right click on a vertex
rightClick = null;
}
});
after hearing back from ESRI it seems they do not provide this detail in their API so this is not possible yet.
I ended up doing this differently. I wanted to add a UI so the user could enter the XY of the point
// setup to allow editing
this.editToolbar = new EditToolbar(this.map, { allowDeleteVertices: false });
const rcMenuForGraphics = new RightClickVertexContextMenu();
const menu = rcMenuForGraphics.createMenu();
// bind to the map graphics as this is where the vertex editor is
this.map.graphics.on("mouse-over", (evt)=> {
// bind to the graphic underneath the mouse cursor
menu.bindDomNode(evt.graphic.getDojoShape().getNode());
});
this.map.graphics.on("mouse-out", (evt)=> {
menu.unBindDomNode(evt.graphic.getDojoShape().getNode());
});
this.editToolbar.on("vertex-click", (evt2) => {
rcMenuForGraphics.setCurrentTarget(evt2);
// evt2.vertexinfo.graphic.geometry.setX(evt2.vertexinfo.graphic.geometry.x - 1000);
})
// when the graphics layer is clicked start editing
gl.on("click", (evt: any) => {
this.map.setInfoWindowOnClick(false);
// tslint:disable-next-line: no-bitwise
const t: any = EditToolbar.MOVE | EditToolbar.EDIT_VERTICES;
this.editToolbar.deactivate();
this.editToolbar.activate(t, evt.graphic);
})
The code for the menu uses esri's vertex editor to grab the point, change its XY and then manually call the events to refresh the geometry. Only tested with polygon
import Menu = require("dijit/Menu");
import MenuItem = require("dijit/MenuItem");
import Graphic = require("esri/graphic");
import Edit = require("esri/toolbars/edit");
import Point = require("esri/geometry/Point");
class RightClickVertexContextMenu {
private curentTarget: { graphic: Graphic; vertexinfo: any; target: Edit; };
public createMenu() {
const menuForGraphics = new Menu({});
menuForGraphics.addChild(new MenuItem({
label: "Edit",
onClick: () => {
// this is a bit hooky. We grab the verx mover, change the x/y and then call the _moveStopHandler
console.log(this.curentTarget.vertexinfo);
const e: any = this.curentTarget.target;
const mover = e._vertexEditor._findMover(this.curentTarget.vertexinfo.graphic);
const g: Graphic = mover.graphic;
// add in a UI here to allow the user to set the new value. This just shifts the point to the left
g.setGeometry(new Point(mover.point.x - 1000, mover.point.y ))
e._vertexEditor._moveStopHandler(mover, {dx: 15});
this.curentTarget.target.refresh();
}
}));
menuForGraphics.addChild(new MenuItem({
label: "Delete",
onClick: () => {
// call the vertex delete handler
const ct: any = this.curentTarget.target;
ct._vertexEditor._deleteHandler(this.curentTarget.graphic)
}
}));
return menuForGraphics;
}
public setCurrentTarget(evt: { graphic: Graphic; vertexinfo: any; target: Edit; }) {
this.curentTarget = evt;
}
}
export = RightClickVertexContextMenu;
Related
On the project where I work (React, TS), we use the viewer and added the Box Selection extension for it.
The first time you activate it with a button in the toolbar, the extension works, the elements are highlighted. Then you can switch to another mode, for example, the orbit mode. And after that, when you click on the button that activates the "box Selection extension", the extension no longer works. The orbit mode remains working.
At the same time, the button is clicked (console.log() is fired) and the loadExtension('Autodesk.Box Selection') method works.
What could be the problem?
I will give some code snippets
This is the extension code:
export default function RectangleSelectionExtension(
this,
viewer,
options,
) {
window.Autodesk.Viewing.Extension.call(this, viewer, options);
}
RectangleSelectionExtension.prototype = (
Object.create(window.Autodesk.Viewing.Extension.prototype)
);
RectangleSelectionExtension.prototype.constructor = RectangleSelectionExtension;
RectangleSelectionExtension.prototype.load = () => true;
RectangleSelectionExtension.prototype.unload = () => true;
RectangleSelectionExtension.prototype.onToolbarCreated = function onToolbarCreated() {
this.group = this.viewer.toolbar.getControl('allExtensionsToolbar');
if (!this.group) {
this.group = new window.Autodesk.Viewing.UI.ControlGroup('allExtensionsToolbar');
this.viewer.toolbar.addControl(this.group);
}
// Add a new button to the toolbar group
this.button = new window.Autodesk.Viewing.UI.Button('RectangleSelectionExtension');
this.button.onClick = async () => {
const boxSelectionExtension = await this.viewer.loadExtension('Autodesk.BoxSelection');
this.viewer.toolController.activateTool(boxSelectionExtension.boxSelectionTool.getName());
boxSelectionExtension.addToolbarButton(this.viewer);
};
this.button.setToolTip('Select within a rectangle area');
this.button.addClass('RectangleSelectionExtension');
this.group.addControl(this.button);
};
window.Autodesk.Viewing.theExtensionManager.registerExtension('BoxSelection', RectangleSelectionExtension);
Next, in the Viewer component, we import and register the extension:
window.Autodesk.Viewing.theExtensionManager.registerExtension('RectangleSelectionExtension', RectangleSelectionExtension);
And this is how we initialize the viewer:
window.Autodesk.Viewing.Initializer(options, () => {
const container = document.getElementById('forgeViewer');
if (container) {
viewer = new window.Autodesk.Viewing.GuiViewer3D(
container,
{
token,
extensions: [
/* ...some extensions */
'RectangleSelectionExtension',
],
},
);
const startedCode = viewer.start();
if (startedCode > 0) {
return;
}
/* ...some eventListeners */
}
I'm not sure I understand the purpose of your RectangleSelectionExtension. From the code it looks like it just adds a button in the toolbar, and clicking that button repeatedly loads another extension (Autodesk.BoxSelection), repeatedly activates the box selection tool, and repeatedly adds the box selection button to the toolbar. That doesn't seem right.
If you're simply interested in the box selection, you can load it (and include it in the toolbar) like so:
// ...
viewer = new window.Autodesk.Viewing.GuiViewer3D(
container,
{
token,
extensions: [
/* ...some extensions */
'Autodesk.BoxSelection',
]
}
);
// and later ...
const boxSelectionExt = viewer.getExtension('Autodesk.BoxSelection');
boxSelectionExt.addToolbarButton(true); // Add the button to the toolbar
boxSelectionExt.addToolbarButton(false); // Remove the button from the toolbar
// ...
In the mxgraph engine, I found the mouse event, but it doesn't reflect the actual map coordinate.
Here is a sample code where evt doesn't fit me.
We need a different solution. We need to get the map coordinate. We can check this by getting negative coordinates (probably).
In general, the task looks like this: the user selects a category of objects, he is shown a modal window with available SVG icons for placement. After the SVG click, the icon is placed in the place where the user clicks. I know that there is a possibility of standard Drag&Drop in mxGraph, but it does not suit us because of the specifics of the project.
import {ref} from "vue";
let dialogVisible = ref(false)
let dialogConfirmed = ref(false)
let selectedItem = ref(false)
let dialogPromise = ref({
resolve: () => {}
reject: () => {}
});
export function useDialog(){
return {
dialogConfirmed,
dialogVisible,
selectedItem,
dialogPromise
}
}
The code below shows the coordinates that we found in mxGraph, but they do not allow us to place the object on click.
this.graph.addListener(MxEvent.CLICK, function(sender, evt)){
console.log('evtOffsetX: ', evt.properties.event.offsetX)
console.log('evtOffsetY: ', evt.properties.event.offsetY)
}
this.graph.getSelectionModel().addListener(MxEvent.CHANGE, function(vertex)){
console.log('vertexCenterX: ', vertex.cells[0].geometry.getCenterX());
console.log('vertexCenterY: ', vertex.cells[0].geometry.getCenterY());
console.log('vertexHeight: ', vertex.cells[0].geometry.height);
console.log('vertexWidth: ', vertex.cells[0].geometry.width);
console.log('vertexX: ', vertex.cells[0].geometry.x);
console.log('vertexY: ', vertex.cells[0].geometry.y);
}
I tried this on mxgraph 4.2.2 and it worked:
this.graph.addListener(mxEvent.CLICK, function(sender, evt) {
console.log("GRAPH CLICK EVENT", sender, evt);
var parent = sender.getDefaultParent();
sender.getModel().beginUpdate();
sender.insertVertex(parent, null, 'TEST', evt.getProperty("event").offsetX, evt.getProperty("event").offsetY, 80, 30);
sender.getModel().endUpdate();
evt.consume();
});
I'm currently working on a floor plan using the npm version of the OpenLayers library (v4.6.4) and wrote some custom controls for the map overlay.
After realizing that hovering over some custom controls will also trigger the forEachFeatureAtPixel hover function I implemented for specific features, I decided to place all map controls in containers outside of the map to prevent the hover trigger when an overlay element is in front of the feature.
Doing so with the basic controls of OpenLayers by adding the target parameter works fine. However, that doesn't seem to work with my custom controls. When calling my custom controls with a target parameter, the following error is thrown:
Uncaught TypeError: this.setTarget is not a function at RotateLeft._ol_control_Control_ (control.js?8790:70)
The mentioned line in ol/control/control.js looks as follows:
if (options.target) {
this.setTarget(options.target);
}
I don't think that there's an error in the control.js, because the standard controls work just fine with the setTarget function.
Here's one of my custom control functions (it adds a button for rotating the view counterclockwise):
/**
* Import OL classes
*/
#import ol from 'ol';
#import Control from 'ol/control/control';
/**
* Import view variable from app
*/
#import {view} from '../../app';
/**
* #constructor
* #extends {ol.control.Control}
* #param {Object=} opt_options Control options.
*/
export function RotateLeft(opt_options) {
let options = opt_options ? opt_options : {};
let rotateLeftButton = document.createElement('button');
rotateLeftButton.setAttribute('title', 'rotate left');
rotateLeftButton.innerHTML = '<i class="fas fa-undo"></i>';
let handleRotateLeft = function() {
view.animate({
rotation: view.getRotation() + (-Math.PI / 2),
});
};
rotateLeftButton.addEventListener('click', handleRotateLeft, {passive: true});
rotateLeftButton.addEventListener('touchstart', handleRotateLeft, {passive: true});
let element = document.createElement('div');
element.className = 'ol-rotate-left ol-unselectable ol-control';
element.appendChild(rotateLeftButton);
Control.call(this, {
element: element,
target: options.target,
});
ol.inherits(RotateLeft, Control);
}
It looks similar to the example given on the OpenLayers example.
And adding it to the map the same way I added the standard controls:
map.addControl(new RotateLeft({
target: document.getElementById('control-rotation-left'),
});
I can't find anything on the internet or here on StackOverflow that handles a similar problem. Does anyone of you have any idea what may cause this error?
Move the call to ol.inherits outside your RotateLeft function:
export function RotateLeft(opt_options) {
let options = opt_options ? opt_options : {};
let rotateLeftButton = document.createElement('button');
rotateLeftButton.setAttribute('title', 'rotate left');
rotateLeftButton.innerHTML = '<i class="fas fa-undo"></i>';
let handleRotateLeft = function() {
view.animate({
rotation: view.getRotation() + (-Math.PI / 2),
});
};
rotateLeftButton.addEventListener('click', handleRotateLeft, {passive: true});
rotateLeftButton.addEventListener('touchstart', handleRotateLeft, {passive: true});
let element = document.createElement('div');
element.className = 'ol-rotate-left ol-unselectable ol-control';
element.appendChild(rotateLeftButton);
Control.call(this, {
element: element,
target: options.target,
});
}
ol.inherits(RotateLeft, Control);
OpenLayer's inheritance system requires that you declare which class your new control inherits from when the new class is declared. It's this ol.inherits call that attaches all the Control methods onto your RotateLeft class, as you were doing this in your RotateLeft function the Control methods - specifically setTarget - aren't available when Control.Call is called, and Control.Call expects setTarget to be available on this.
I have a JavaScript Listener within a Google Maps object:
map.data.addListener('click', (event) => {
var mapElement = event.feature.getProperty('type');
switch(mapElement){
case 'cameras':
var cameraID = event.feature.getProperty('trafficID');
this.cameraService.createCamera(cameraID);
break; ...
This 'map' Listener calls a function within a service called 'cameraService' which initialises the camera. I have a DOM object which uses Angular 2's ngIf directive to determine whether it exists:
<div *ngIf="cameraService.camera1 != null">
<app-camera></app-camera>
</div>
My problem: ngIf doesn't run after the click event happens. Console logs show that the camera object is created, and if I click on another element (outside of the map), the view updates (ngIf change detection starts up again).
I assume this issue to do with the Angular 2 zone's - so when I click on the Google Map, it is outside of the Angular 2 zone and when I click on another element (within Angular 2), I re-enter the Angular 2 zone [Correct me if I am wrong!]... How do I manually trigger ngIf or re-enter the Angular 2 zone at the end of the click event to restart the Angular 2 change detection?
You can either use zone.run() to force the execution back into Angulars zone or you can invoke change detection manually:
constructor(private zone:NgZone, private cdRef:ChangeDetectorRef) {}
map.data.addListener('click', (event) => {
this.zone.run(() => {
var mapElement = event.feature.getProperty('type');
switch(mapElement){
case 'cameras':
var cameraID = event.feature.getProperty('trafficID');
this.cameraService.createCamera(cameraID);
break; ...
});
}
or
map.data.addListener('click', (event) => {
var mapElement = event.feature.getProperty('type');
switch(mapElement){
case 'cameras':
var cameraID = event.feature.getProperty('trafficID');
this.cameraService.createCamera(cameraID);
this.cdRef.detectChanges(); // <<<=== added
break; ...
}
I've been working on a context menu module in ReactJS, and it's got me thinking about how to deal with non-hierarchical components.
The problem I'm running into is that many different items in an application may want to use a context menu. Normally in React, you pass a callback from a parent object to the children that need to communicate with the parent. For example, my first thought was to have an openContextMenu(mousePosition, optionsObject) function passed from my ContextMenu class to all the elements that want to display a context menu on right-click.
But it doesn't make sense for all such elements (or maybe even any) to be children of a context menu! The context menu is not hierarchical with respect to other components of the application. In Angular, I would probably write a ContextMenu service that components required if they wanted access to such a menu.
Is this a situation in which a global event handler should be used? Am I thinking about this all wrong? What's the React way to handle this kind of horizontal interaction between components?
Context menus are special. There should never be more than one context menu open at any time. They're also special because it can be opened from anywhere. Try the demo to get an idea of how this looks when put together.
To solve our global problem, we'll create a mixin which wraps a private event emitter.
var menuEvents = new events.EventEmitter();
var ContextMenuMixin = {
// this.openContextMenu(['foo', 'bar'], (err, choice) => void)
openContextMenu: function(options, callback){
menuEvents.emit('open', {
options: options,
callback: callback
});
},
closeContextMenu: function(){
menuEvents.emit('close');
}
};
Now for the component, we need to do a few things. Here's the initialization part. Just binding to some events, and lightweight mouse tracking.
var mouse = {x: 0, y: 0};
var updateMouse = function(e){
mouse.x = e.pageX;
mouse.y = e.pageY;
};
var ContextMenu = React.createClass({
getInitialState: function(){
return {options: null, callback: null};
},
componentDidMount: function(){
menuEvents.addListener('open', this.handleOpenEvent);
menuEvents.addListener('close', this.closeMenu);
addEventListener('mousemove', updateMouse);
},
These event handlers are very simple. handleOpenEvent just stores the event payload and mouse position in state, which effectively locks in the mouse position until it's opened next. And the counterpart simply resets the state, and calls the callback with an error.
handleOpenEvent: function(payload){
this.setState(_.merge({}, payload, mouse));
},
closeMenu: function(){
if (this.state.callback) {
this.replaceState(this.getInitialState());
this.state.callback(new Error('no selection made'));
}
},
And finally, we render a list of options passed to the event, and we create click handlers for each.
render: function(){
if (!this.state.options) {
return <div />
}
var style = {
left: this.state.x,
top: this.state.y,
position: 'fixed'
};
return (
<div className="react-contextmenu" style={style}>
<ul className="react-contextmenu-options">
{this.state.options.map(function(x, i){
return <li key={i}
onClick={this.makeClickHandler(x)}>
{x}
</li>
}, this)}
</ul>
</div>
);
},
makeClickHandler: function(option){
return function(){
if (this.state.callback) {
this.state.callback(null, option);
this.replaceState(this.getInitialState());
}
}.bind(this);
}