mousemove with two layers on top of each other (overlapping) - javascript

The closest to my question are: Mapbox gl js - overlapping layers and mouse event handling, Mapbox add popup on hover (layer), close on mouseleave, but keep open on popup hover and How to ignore mouse events on a mapbox layer - but they don't answer it.
I have two layers, let's imagine one is for a country, and another is for a city. Both of them a handled like this:
// CountryLayer.js
map.on("mousemove", "country-layer", e => {
// show info about the country
featureProps = e.features[0] // display some notification from the props
...
// CityLayer.js
map.on("mousemove", "city-layer", e => {
// show info about the city
featureProps = e.features[0] // display some notification from the props
...
It's done in different components. But when I mouseover city-layer mapbox thinks that I'm still "mousemoving" on top of the country-layer as well, so I get two notifications from separate components, where I need only one - in that case from the city-layer cause it's on top of country-layer.
Handling the mousemove without layerId in one place is gonna be a mess and breaks all the good rules about programming. Creating external "event manager" which will track whether I'm hovering the city and if is so will remove mousemove event from country-layer - is complex. I didn't find any good alternatives. At least, I would be glad to disable pointer events for a layer like this:
map.on("mousemove", "city-layer", e => {
map.getLayer("country-layer").setStyle({ "pointer-events": false })
featureProps = e.features[0]
...
or something like this. Is it possible? Is there more adequate way around it?
e.originalEvent.stopPropagation(); does not work

It appears, that you can do e.originalEvent.preventDefault(); in the city-layer and add a check e.originalEvent.defaultPrevented in the country-layer from the example.
However, I have issues with z-index position of layers: map.moveLayer("city-layer", "country-layer"); or vice-vise doesn't actually change the way, event propogates, so for some reason my country-layer always come first, so when it checks for e.originalEvent.defaultPrevented, preventDefault() wasn't fired yet, so it comes always false.
But technically, this answers my question.

map.on can listen on multiple layers at once. With events like mousemove it returns features in reverse layer order (from "highest" to "lowest").
So the way to do this is:
map.on("mousemove", ["city-layer", "country-layer"], e => {
feature = e.features[0]
if (feature.layer.id === 'city-layer') {
// ...
} else {
// ...
}
});

Related

How to close an info bubble when there is a click outside the info bubble and marker - HERE maps API

I have been trying to close an info bubble when the map only is clicked, but I can't achieve it.
I have tried the following
this.map.addEventListener("tap", this.handleMapClick);
private handleMapClick(evt: Event) {
this.clearOpenInformationBubble();
}
However, the tap event is triggered even when maps objects are clicked meaning that the info bubble remains closed when I click a marker.
I have also tried to add a 'blur' event listener to the bubble element, but that doesn't seem to work
const bubble = new H.ui.InfoBubble(evt.target.getGeometry(), {
content: ...
});
this.ui.addBubble(bubble);
bubble.getElement().focus()
bubble.getElement().addEventListener('blur', evt => {
this.clearOpenInformationBubble();
})
I was wondering if there is a way to listen for an event triggered by a map ONLY tap.
Here's a similar implementation in Google maps.
google.maps.event.addDomListener(map, "click", function() {
alert('Map clicked')
});
I think I'd try the answer here. In this answer, they wanted to close any other open infobubbles first. I believe if you remove the event listener on your group, keep the one on your map, and always remove open infobubbles you would be good. Then add logic to see if the event target value has data and show an infobubble.
This assumes your doing infobubbles based on markers having a data object associated with them.
Edit:
I was able to get this to work - again assuming your use case is markers to infobubbles.
map.addEventListener('tap', evt => {
ui.getBubbles().forEach(bub => ui.removeBubble(bub));
if(!evt.target.getData) return;
// for all objects that it contains
var bubble = new H.ui.InfoBubble(evt.target.getGeometry(), {
// read custom data
content: evt.target.getData()
});
// show info bubble
ui.addBubble(bubble);
});
You can check the event target in the callback function.
If it is the map, then only you close the InfoBubble:
this.map.addEventListener("tap", this.handleMapClick);
private handleMapClick(evt: Event) {
if (evt.target === this.map) {
this.clearOpenInformationBubble();
}
}
```

Handling event listeners on multiple pages

I've written a javascript file which has some event listeners in for things like tabs, accordions and so on. However these aren't present in every single page so it's looking for elements that don't exist and throws the entire js out of wack.
I know I could get around it by using multiple if statements, but it doesn't sound like it would be correct.
// Accordion
const accordions = document.querySelectorAll('.accordion li');
accordions.forEach(accordion =>{
accordion.addEventListener('click', (e) => {
e.preventDefault();
accordion.classList.toggle('open');
})
});
// Inline toggle
const inlineToggle = document.getElementById('inline-toggle');
inlineToggle.addEventListener('click', () => {
inlineToggle.nextElementSibling.classList.toggle('active');
});
const inlineToggleOptions = document.querySelectorAll('.inline-toggle-options button');
inlineToggleOptions.forEach(option => {
option.addEventListener('click', (e) => {
// Prevent default
e.preventDefault();
// Update sentence text
inlineToggle.innerHTML = option.dataset.payType;
// Remove selected class from options
inlineToggleOptions.forEach(option => {
option.classList.remove('selected');
});
// Add selected class to chosen option
option.classList.add('selected');
// Close dialog
inlineToggle.nextElementSibling.classList.remove('active');
})
});
// Cover bubbles
// Create the slidepanel
const placeholder = document.getElementById('slidepanel');
// Find all buttons
const button = document.querySelectorAll('.trigger-aside');
button.forEach((button => {
// Listen for clicks on buttons
button.addEventListener('click',(e) => {
// Prevent default
e.preventDefault();
// Get the target
const target = button.dataset.target;
console.log(target);
// Call the API
fetch(`http://****.****.uk/****/****/****/${target}`)
.then((res) => res.json())
.then(function(res) {
// Load HTML into slider panel
placeholder.innerHTML = res.object.content;
// Stop body overflow
document.body.classList.add('overflow-hidden');
// Create overlay and append
const overlay = document.querySelector('.overlay');
overlay.classList.add('active');
document.body.appendChild(overlay);
// Show the panel
placeholder.classList.add('active');
document.body.appendChild(placeholder);
// Listen for close
overlay.addEventListener('click', (e) =>{
// Close requested
document.body.classList.remove('overflow-hidden');
placeholder.classList.remove('active');
overlay.classList.remove('active');
});
})
.catch(function(err) {
// Log error
console.log(err);
});
})
}));
How do other people generally get around this issue? Any guidance appreciated!!
Event Delegation Pattern
If you have these UI elements grouped under a parent element in common across all pages(like a div wrapper), you could try making use of the Event Delegation pattern. Essentially, you can assign a click event to that parent element and make use of a function to only take action if the desired element is returned - i.e. your buttons. It would go something like...
const parent = document.querySelector('div.wrapper'); //Change selector to suit a common parent
const buttons = [...document.querySelectorAll('.inline-toggle-options button')]; // convert to array to make it easier to work with
const elementsToChange = document.querySelectorAll('.elements .to .change');
parent.addEventListener('click', toggleOptions);
function getEventTarget(e) {
e = e || window.event;
return e.target || e.srcElement; // For IE compatibility
}
function toggleOptions {
let target = getEventTarget(e);
if(buttons.includes(target)) {
// Trigger options on UI elements if any of the buttons are among the clicked elements
// Target refers to the buttons in particular, not the UI elements you want to change
}
}
Whichever way you want to refactor the code to take action on specific elements is up to you. You can group buttons by specific functionality into distinct arrays. If you had 2 or 3 arrays, you'd only need to write 2 or 3 options from a conditional statement.
For this purpose, you'll also save memory with this pattern since you're only assigning one event handler and letting child events on child elements bubble up to be taken care of by that single handler. Also, you shouldn't run into errors since the initial event handler is assigned to a parent element that's common across all pages.
Some Caveats
From the guide linked above:
Not all events bubble. The blur, focus, load and unload events don’t
bubble like other events. The blur and focus events can actually be
accessed using the capturing phase (in browsers other than IE) instead
of the bubbling phase but that’s a story for another day.
You need caution when managing some mouse events. If your code is
handling the mousemove event you are in serious risk of creating a
performance bottleneck because the mousemove event is triggered so
often. The mouseout event has a quirky behaviour that is difficult to
manage with event delegation.
I'd generally advise to think first about code organisation and second about efficiency. I agree with StevenB.'s suggestion and Luke Tubby's answer, to make the best of your current situation with a minimum of effort.
For more elaborate solutions, I would suggest to familiarise with build / packaging tools (e.g. Webpack ), that offer you ways to structure your code (and assets) in files and directories of your choice, and create page specific minified packages.
Another (completely different and independent) approach to the problem (of improving efficiency and code organisation) would be to build a single page application...

leaflet.pm edit mode get new coordinates within shape

I'm using leaflet js to build a map with some pins https://leafletjs.com/ and I'm also allowing drawing of shapes, e.g. polygons, circles etc. I also these to be edited using a plugin called leaflet.pm https://github.com/codeofsumit/leaflet.pm.
There are events here but none of the events are giving be back the cordinates of the new position after edit mode has been disabled or after the drag has finished. This is the event I have hooked into;
map.on('pm:globaleditmodetoggled', function(e) {
console.log(e);
});
Wheres this event gives me what is required;
map.on('pm:create', function(e) {
let obj = {
type: e.shape,
coordinates: e.layer.editing.latlngs[0][0]
};
$('#cords').val(JSON.stringify(obj))
});
Any ideas how when the shape is edited that I can get the update coordinates?
I'm Sumit, the maintainer of leaflet.pm
What you can do is: listen to an event being created and add the edit event to the new shapes:
map.on('pm:create',(e) {
e.layer.on('pm:edit', ({ layer }) => {
// layer has been edited
console.log(layer.toGeoJSON());
})
});
Of course, whenever you add a layer to the map you can also apply the pm:edit event to its reference.
Additionally, when you create layers or add layers to the map, you can simply store the reference. When editing is done you can just check the reference for it's coordinates (just as you would normally in leaflet). If you just need to know when editing is done, use the pm:edit event to catch whenever a layer was edited.
Hope this helps 👍

AmMaps is not registering clickMapObject

I'm having trouble getting AmMaps to execute a function when I click on a state. Here's my code. This line is not operating as I expect it to:
map.addListener("clickMapObject", function(event) {
When I shorten "clickMapObject" to just "click", the function works fine. But I need clickMapObject so the map will be able to tell me which state I've clicked on; if I use click it will only give me lat/long coordinates. What am I doing wrong?
Got a response back from AmCharts support:
Just to clarify, if the area/state/country does not have any actions
attach to it upon click (zoom, url, description, etc.) it does not
generate any click events.
In this case you need to explicitly make all such "inactive" areas
clickable by either setting "selectable" property for each area
individually or globally via AreasSettings:
http://docs.amcharts.com/3/javascriptmaps/MapArea#selectable
http://docs.amcharts.com/3/javascriptmaps/AreasSettings#selectable
The latter is probably more universal if you want all areas to
generate click event:
map.areasSettings = {
rollOverColor: "#009ce0"
selectable: true
};
I hope it helps.
And it did help. Adding selectable: true to my map.areasSettings object did the trick!

jVectorMap generates a 100x100 when display object is initially hidden

So I'm having a problem generating my jVectorMap.
The map itself sits inside a very custom drop down menu that I have created and this is where I suspect the problem is.
When I mouseover my menu item to open up the drop down which contains the map the actual svg starts out with a forced dimension of 100px x 100px.
What I have tried to do a number of workarounds wher I call the "map.setSize()" either on the mouseclick event of the dropdown as well as the mouseover event of the container itself. The problem here is my dropdown is not subject to a click event but shows on the mouseover event. However, at the point of the mouseover event the actual container for the map hasn't loaded so I'm still stuck with a 100px x 100px svg.
To get around this I've put an event on the mouseover event of the container itself but this isn't great either as it then requires the user to move his mouse over the container before it actually shows the map, something I don't want to happen.
Is there a way of getting the map built inside a div which is invisible before my menu event occurs?
For an example of my problem I've created this at jsfiddle http://jsfiddle.net/AEup9/
You will notice that when you hover over the "Show Map" menu item (the only item) the drop down is blank except for the topic headers until you move the mouse over the actual drop down itself which then reloads the map. I then keep the map there by using my "loaded" variable created before my mouseover event and a force map.setSize() inside the same event:
var loaded = false;
$('#aamap').mouseover(function () {
if (!loaded) {
(function () {
map = new jvm.WorldMap({
map: 'za_mill_en',
container: $('#southafrica-map'),
backgroundColor: '#cbd9f5',
initial: {
fill: 'white'
},
series: {
regions: [{
attribute: 'stroke'
}]
}
});
loaded = true;
})();
}
map.setSize();
});
This is my rough work around but not what I really want as I want the map to show up first time.
Can anyone help me here?
Edit: I finally decided to NOT go ahead with using jvectormap due to this issue. Instead I opted to use jqvmap which is to some degree a fork of jvectormap, however the issues experienced with jvectormap were no longer a problem.
I met this issue as well.
To solve that problem we need to run an updateSize method on a map object when container of our map becomes visible. First, to get the map object we need to use this command:
$('#world-map').vectorMap('get', 'mapObject') and when execute updateSize on it, like:
var map = $('#world-map').vectorMap('get', 'mapObject');
map.updateSize();
or in a shorter form:
$('#world-map').vectorMap('get', 'mapObject').updateSize();

Categories

Resources