Stop Event Propagation of Mapbox Layers - javascript

I have one layer and a basemap
mapboxgl.accessToken = '';
const coords = JSON.parse('{"type":"FeatureCollection","features":[{"type":"Feature","properties":{},"geometry":{"type":"Point","coordinates":[13.380550656438709,52.52208508665396]}},{"type":"Feature","properties":{},"geometry":{"type":"Point","coordinates":[13.380633221743006,52.52208172104466]}},{"type":"Feature","properties":{},"geometry":{"type":"Point","coordinates":[13.380686171093972,52.52208244564463]}},{"type":"Feature","properties":{},"geometry":{"type":"Point","coordinates":[13.380702060621635,52.5220511942754]}},{"type":"Feature","properties":{},"geometry":{"type":"Point","coordinates":[13.380527236009051,52.52205779286111]}}]}');
this.map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v9',
zoom: 19, // starting zoom
center: [13.380702060621635, 52.5220511942754]
});
this.map.on('load', async () => {
const controls = new mapboxgl.NavigationControl();
this.map.addControl(controls, 'top-right');
this.map.addSource('foo', {
type: 'geojson',
data: coords
});
this.map.addLayer({
id: 'points',
type: 'circle',
source: 'foo',
paint: {
'circle-radius': 5,
'circle-color': 'hotpink',
},
});
});
this.map.on('click', 'points', event => {
console.log('Layer click')
});
this.map.on('click', () => {
console.log('Basemap click')
});
body {
margin: 0;
padding: 0;
}
#map {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
#sidebar {
background-color: hotpink;
height: 100vh;
width: 200px;
position: relative;
z-index: 99999999999;
opacity: 0.5;
}
.popup-content {
display: inline-block
}
.pin-icon {
font-size: 20px;
color: blue
}
.vl {
border-left: 1px solid #bababa;
height: 15px;
padding: 0px;
margin: 10px;
}
<html>
<head>
<meta charset='utf-8' />
<title></title>
<meta name='viewport' content='initial-scale=1,maximum-;scale=1,user-scalable=no' />
<script src='https://api.tiles.mapbox.com/mapbox-gl-js/v1.2.0/mapbox-gl.js'></script>
<link href='https://api.tiles.mapbox.com/mapbox-gl-js/v1.2.0/mapbox-gl.css' rel='stylesheet' />
</head>
<body>
<div id='map'></div>
</body>
</html>
https://codepen.io/diesdasananas/pen/eqVLyj
Whenever I click on the circle layer, the event propagates through the basemap. Basemap click gets logged. I wonder how do I stop event propagation from the circle layer to the basemap? I cannto use event.stopPropagation() because Mapbox draws on the canvas...

One approach is to save the event coordinates of the click on the layer and then compare these coordinates with the underlying layer and its event coordinates.
let clickCoords = {};
this.map.on('click', 'points', event => {
clickCoords = event.point;
console.log('Layer click')
});
Now, detect a click on the map, not on the points layer
this.map.on('click', () => {
// check if coords are different, if they are different, execute whatever you need
if (clickCoords.x !== event.point.x && clickCoords.y !== event.point.y) {
console.log('Basemap click');
clickCoords = {};
}
});
Or, even better, use queryRenderedFeatures().
this.map.on('click', event => {
if (this.map.queryRenderedFeatures(event.point).filter(feature => feature.source === YOURLAYERNAME).length === 0) {
console.log('Basemap click');
}
});
Here is a Mapbox example.

Related

Can't get Google map to display

Working from Google-generated code, I am trying to get a Google map to display on a page for a user to enter/select an address using Google Places. The relevant HTML/CSS code is as follows:
<style>
.map {
width: 300px;
}
.card-container {
display: flex;
height: 500px;
width: 600px;
}
.panel {
background: white;
width: 300px;
padding: 20px;
display: flex;
flex-direction: column;
justify-content: space-around;
}
</style>
<div class="card-container">
<div class="panel">
<div class="map" id="gmp-map"></div>
</div>
</div>
The script code for this page is as follows:
<script>
$(document).ready(function () {
"use strict";
$('#location-input').focus();
})
function initMap() {
const CONFIGURATION = {
"ctaTitle": "Checkout",
"mapOptions": { "center": { "lat": 37.4221, "lng": -122.0841 }, "fullscreenControl": true, "mapTypeControl": false, "streetViewControl": true, "zoom": 11, "zoomControl": true, "maxZoom": 22, "mapId": "" },
"mapsApiKey": "YOUR_API_KEY",
"capabilities": { "addressAutocompleteControl": true, "mapDisplayControl": true, "ctaControl": true }
};
const componentForm = [
'location',
'street_number',
'route',
'locality',
'administrative_area_level_1',
'postal_code',
];
const getFormInputElement = (component) => document.getElementById(component + '-input');
const map = new google.maps.Map(document.getElementById("gmp-map"), {
zoom: CONFIGURATION.mapOptions.zoom,
center: { lat: 37.4221, lng: -122.0841 },
mapTypeControl: false,
fullscreenControl: CONFIGURATION.mapOptions.fullscreenControl,
zoomControl: CONFIGURATION.mapOptions.zoomControl,
streetViewControl: CONFIGURATION.mapOptions.streetViewControl
});
const marker = new google.maps.Marker({ map: map, draggable: false });
const autocompleteInput = getFormInputElement('location');
const autocomplete = new google.maps.places.Autocomplete(autocompleteInput, {
fields: ["address_components", "geometry", "name", "formatted_address"],
types: ["address"],
});
autocomplete.addListener('place_changed', function () {
marker.setVisible(false);
const place = autocomplete.getPlace();
if (!place.geometry) {
$('#addrErr').text('No details available for input: \'' + place.name + '\'')
$('#toastAddrErr').toast('show');
return;
}
$('#longAddress').val(place.formatted_address);
renderAddress(place);
fillInAddress(place);
});
function fillInAddress(place) { // optional parameter
const addressNameFormat = {
'street_number': 'short_name',
'route': 'long_name',
'locality': 'long_name',
'administrative_area_level_1': 'short_name',
'postal_code': 'short_name',
};
const getAddressComp = function (type) {
for (const component of place.address_components) {
if (component.types[0] === type) {
return component[addressNameFormat[type]];
}
}
return '';
};
getFormInputElement('location').value = getAddressComp('street_number') + ' ' + getAddressComp('route');
for (const component of componentForm) {
if (component !== 'location') {
getFormInputElement(component).value = getAddressComp(component);
}
}
}
function renderAddress(place) {
alert(place.geometry.location);
map.setCenter(place.geometry.location);
marker.setPosition(place.geometry.location);
marker.setVisible(true);
}
}
</script>
<script src="https://maps.googleapis.com/maps/api/js?key=<MY_KEY_REMOVED_FOR_QUESTION>&libraries=places&callback=initMap&solution_channel=GMP_QB_addressselection_v1_cABC"></script>
I have removed my API key, by the way.
In any event, everything works as it should, and the text boes displaying address information (not included here) properly populate from the callback, except that the map never renders when the page loads (there's a default already loaded), and it doesn't render after the user enters and selects an address.
There are no messages in the console, and when I put alerts in the renderAddress() function to see if it's even being reached, they display correct information. The map is failing silently, and I don't know why.
Any thoughts?

How to locate a user without clicking on the control button? (MapBox API, JavaScript)

I'm trying to locate the user when the website is fully loaded.
I'm using the newest MapBox API (JavaScript)
Is it possible to do that without requiring the user to click on the top right button on the map?
var map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v11',
center: [0,0],
zoom: 15 // starting zoom
});
// Add geolocate control to the map.
map.addControl(new mapboxgl.GeolocateControl({
positionOptions: {
enableHighAccuracy: true
},
trackUserLocation: true
}));
try with this example, it's work for me
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8' />
<title></title>
<meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
<script src='https://cdnjs.cloudflare.com/ajax/libs/mapbox-gl/0.53.1/mapbox-gl.js'></script>
<link href='https://cdnjs.cloudflare.com/ajax/libs/mapbox-gl/0.53.1/mapbox-gl.css' rel='stylesheet' />
<style>
body { margin:0; padding:0; }
#map { position:absolute; top:0; bottom:0; width:100%; }
</style>
<script >
var get_location = function() {
var geolocation = null;
var c_pos = null;
if (window.navigator && window.navigator.geolocation) {
geolocation = window.navigator.geolocation;
var positionOptions = {
enableHighAccuracy: true,
timeout: 10 * 1000, // 10 seconds
maximumAge: 30 * 1000 // 30 seconds
};
function success(position) {
console.log(position);
c_pos = position.coords;
mapboxgl.accessToken = 'token'; ///////////////// put your token here
if (!mapboxgl.supported()) {
alert('Your browser does not support Mapbox GL');
} else {
var map = new mapboxgl.Map({
container: 'map', // container id
style: 'mapbox://styles/mapbox/streets-v11',
center: [c_pos.longitude, c_pos.latitude],
zoom: 12 // starting zoom
});
}
}
function error(positionError) {
console.log(positionError.message);
}
if (geolocation) {
geolocation.getCurrentPosition(success, error, positionOptions);
}
} else {
alert("Getting Geolocation is prevented on your browser");
}
return c_pos;
}
</script>
</head>
<body>
<div id='map'></div>
<script>
var current_pos = get_location();
</script>
</body>
</html>
try with this
navigator.geolocation.getCurrentPosition(position => {
const userCoordinates = [position.coords.longitude, position.coords.latitude];
map.addSource("user-coordinates", {
type: "geojson",
data: {
type: "Feature",
geometry: {
type: "Point",
coordinates: userCoordinates
}
}
});
map.addLayer({
id: "user-coordinates",
source: "user-coordinates",
type: "circle"
});
map.flyTo({
center: userCoordinates,
zoom: 14
});
});
Yes, you have to use trigger() to activate the tracking in a programmed way.
// Initialize the geolocate control.
var geolocate = new mapboxgl.GeolocateControl({
positionOptions: {
enableHighAccuracy: true
},
trackUserLocation: true
});
// Add the control to the map.
map.addControl(geolocate);
map.on('load', function() {
geolocate.trigger(); //<- Automatically activates geolocation
});
see https://docs.mapbox.com/mapbox-gl-js/api/markers/#geolocatecontrol-instance-members

introjs onafterchange fires before tooltip is positioned

While using introjs.js, I am trying to set the position of a tooltip (.introjs-tooltip) but, if I use the onafterchange event, my code runs, and then the position of the tooltip is set by introjs, and my values for top and left are overwritten. How can I make my change AFTER introjs has done it's calculations for the location of the tooltip?
<div>
<div class="divStep step1">
<span>Nothing much going on here</span>
</div>
<div id="step2" class="divStep step2">
<span>This is step 2</span>
</div>
<div id="step3" class="divStep step3">
<span>This is step 3</span>
</div>
</div>
body {
background-color: #00eeee;
}
.divStep {
display: block;
height: 100px;
width: 400px;
background-color: #fff;
margin: 10px;
padding: 10px;
}
.tt-step2 {
top: 0 !important;
left: 0 !important;
background-color: #ff0000;
}
var myIntro = {
tooltipPosition: 'bottom',
steps: [
{
intro: 'Howdy! This is step 1'
},
{
element: '#step2',
intro: 'This is step 2',
onbeforechange: function(){
console.log('onbeforechange step 2');
$('.introjs-tooltip').addClass('tt-step2');
console.log('has class? ' + $('.introjs-tooltip').hasClass('tt-step2'));
},
onchange: function(){
console.log('onchange step 2');
$('.introjs-tooltip').addClass('tt-step2');
console.log('has class? ' + $('.introjs-tooltip').hasClass('tt-step2'));
},
onafterchange: function(){
console.log('onafterchange step 2');
$('.introjs-tooltip').addClass('tt-step2');
console.log('has class? ' + $('.introjs-tooltip').hasClass('tt-step2'));
}
},
{
element: '#step3',
intro: 'This is step 3'
}
]
}
function launchIntro(){
var intro = introJs();
intro.setOptions(myIntro);
intro
.onbeforechange(function(){
currentStep = this._options.steps[this._currentStep];
if(currentStep.onbeforechange) {
currentStep.onbeforechange();
}
})
.onchange(function(){
currentStep = this._options.steps[this._currentStep];
if(currentStep.onchange) {
currentStep.onchange();
}
})
.onafterchange(function(){
currentStep = this._options.steps[this._currentStep];
if(currentStep.onafterchange) {
currentStep.onafterchange();
}
})
.start();
}
launchIntro();
I came across a similar issue. Currently IntroJS library has an open issue on github.
I had an assignment to heavily customize the elements styles and adjust some behavior.
I managed to handle the situation by using MutationObserver
Here's an example snippet:
const observer = new MutationObserver((mutations) => {
const { target } = mutations[0];
document.querySelector('.introjs-tooltip').style.top = `${
document.querySelector('.introjs-helperLayer').clientHeight
- document.querySelector('.introjs-tooltip').clientHeight - 10}px`;
return null; });
observer.observe(
document.querySelector('.introjs-tooltip'),
{ attributes: true, attributeOldValue: true, attributeFilter: ['style'] },
);

How to dynamically define the 'hero' in 'neon-animated-pages'?

I have two buttons in a page and I'd like the clicked one to play as hero in a transition to another page.
Say I have the first page:
<dom-module id="first-page">
<style>
.hero_from_1 {
position: fixed;
bottom: 0;
left: 0;
background-color: cornflowerblue;
}
.hero_from_2 {
position: fixed;
bottom: 0;
right: 0;
background-color: cornflowerblue;
}
</style>
<template>
<paper-button class="hero_from_1" raised on-tap="tapHandler_1">First Hero</paper-button>
<paper-button class="hero_from_2" raised on-tap="tapHandler_2">Second Hero</paper-button>
</template>
<script>
addEventListener('WebComponentsReady', function() {
Polymer({
is: 'first-page',
tapHandler_1: function(ev){
document.getElementsByClassName('hero_from_1')[0].setAttribute('id','hero_from');
document.getElementsByClassName('hero_from_2')[0].removeAttribute('id');
this.fire('second-page');
},
tapHandler_2: function(ev){
document.getElementsByClassName('hero_from_1')[0].removeAttribute('id');
document.getElementsByClassName('hero_from_2')[0].setAttribute('id','hero_from');
this.fire('second-page');
},
behaviors: [Polymer.NeonSharedElementAnimatableBehavior],
properties: {
animationConfig: {
value: function(){
return {
'entry':{
name: 'fade-in-animation',
node: this
},
'exit':[{
name: 'hero-animation',
id: 'hero',
fromPage: this
},{
name: 'fade-out-animation',
node: this
}]
}
}
},
sharedElements: {
value: function() {
return {
'hero': this.$.hero_from // changes in ids do not affect hero!!!
}
}
}
}
})
});
</script>
</dom-module>
with a standard sharedElements property, as documented in the project pages (https://elements.polymer-project.org/guides/using-neon-animations#page-transitions).
The problem is that even if I exchange the id of the buttons right after a click or tap event, the change does not propagate to the sharedElements function, and the animation does not work.
See fiddle:
http://jsfiddle.net/ferdinandkraft/qzxzeb1u/
The problem is that the sharedElements' function is evaluated at creation/attachment time, so it will not keep track of changing ids.
I've found a way to work aroud this, simply by setting the sharedElements property right after the tap event, just like any other property:
<dom-module id="first-page">
<style>
.hero_from_1 {
position: fixed;
bottom: 0;
left: 0;
background-color: cornflowerblue;
}
.hero_from_2 {
position: fixed;
bottom: 0;
right: 0;
background-color: cornflowerblue;
}
</style>
<template>
<paper-button class="hero_from_1" raised on-tap="tapHandler">First Hero</paper-button>
<paper-button class="hero_from_2" raised on-tap="tapHandler">Second Hero</paper-button>
</template>
<script>
addEventListener('WebComponentsReady', function() {
Polymer({
is: 'first-page',
tapHandler: function(ev){
this.sharedElements = {'hero': ev.target}; /////// here!!!
this.fire('second-page');
},
behaviors: [Polymer.NeonSharedElementAnimatableBehavior],
properties: {
animationConfig: {
value: function(){
return {
'entry':{
name: 'fade-in-animation',
node: this
},
'exit':[{
name: 'hero-animation',
id: 'hero',
fromPage: this
},{
name: 'fade-out-animation',
node: this
}]
}
}
},
/* sharedElements removed! */
}
})
});
</script>
</dom-module>
Note that ev.target will be the clicked button, so a single tapHandler will do the job!
See fiddle:
http://jsfiddle.net/ferdinandkraft/b1sw1q2o/

jquery ui autocomplete with multiple fields

this code works just fine, but the second input field does not show images appearing with the text suggestions. I would appreciate if someone could take a look and let me know what needs to be changed in the js for it to work.
Example queries: clinton, bush
you can see the script here http://predcast.com/include/autoc/jqui/test2.php
<!DOCTYPE html>
<html>
<head>
<title>jQuery UI Autocomplete: Custom HTML in Dropdown</title>
<link href="//ajax.googleapis.com/ajax/libs/jqueryui/1.11.1/themes/smoothness/jquery-ui.min.css" rel="stylesheet">
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"> </script>
<script src="//ajax.googleapis.com/ajax/libs/jqueryui/1.11.1/jquery-ui.min.js"></script>
<style>
.loading {
display: none;
width: 16px;
height: 16px;
background-image: url(/img/loading.gif);
vertical-align: text-bottom;
}
#autocomplete.ui-autocomplete-loading ~ .loading {
display: inline-block;
}
.ui-menu-item {
padding:1px;
margin:1px;
}
.ac-m {
height:block;
overflow:auto;
padding:2px 2px 2px 2px;
}
.ac-img {
max-width:30px;
float:left;
margin:2px 2px 2px 2px;
}
.ac-title {
margin:1px;
font-size:14px;
}
.ui-autocomplete {
margin:1px;
}
</style>
</head>
<body>
<form action="http://www.test.com/">
<input class="autocomplete" type="text" placeholder="Option 1" name="e1">
<input class="autocomplete" type="text" placeholder="Option 2" name="e2">
<span class="loading"></span>
</form>
<script>
/*
* jQuery UI Autocomplete: Custom HTML in Dropdown
* http://salman-w.blogspot.com/2013/12/jquery-ui-autocomplete-examples.html
*/
$(function () {
$('.autocomplete').autocomplete({
delay: 500,
minLength: 3,
source: function (request, response) {
$.getJSON("http://predcast.com/include/autoc/jqui/jsond.php", {
q: request.term,
}, function (data) {
var array = data.error ? [] : $.map(data.movies, function (m) {
return {
label: m.title,
year: m.year,
img: m.img,
};
});
response(array);
});
},
focus: function (event, ui) {
event.preventDefault();
},
}).data("ui-autocomplete")._renderItem = function (ul, item) {
var $a = $("<div class='ac-m'></div>");
if (item.img) {
$("<span></span>").addClass(item.icon).appendTo($a).append("<img src='" + item.img + "' border='0' class='ac-img' />");
}
$("<span class='ac-title'></span>").text(item.label).appendTo($a);
return $("<li></li>").append($a).appendTo(ul);
};
});
</script>
</body>
</html>
The problem is related to the way you are defining the _renderItem extension point.
In your code, you are redefining the jquery-ui autocomplete _renderItem function only for your first widget instance, so the _renderItem for your second autocomplete instance is the default one defined in the jquery-ui code.
You are initializating the autocomplete for your 2 inputs with this $('.autocomplete').autocomplete({ ...}) then you get the first widget instance with this instruction .data("ui-autocomplete") and then redefine the _renderItem function for this instance only.
You can define it for all your instances like this:
// Create your widget instances
$('.autocomplete').autocomplete({
delay: 500,
minLength: 3,
source: function (request, response) {
$.getJSON("http://predcast.com/include/autoc/jqui/jsond.php", {
q: request.term,
}, function (data) {
var array = data.error ? [] : $.map(data.movies, function (m) {
return {
label: m.title,
year: m.year,
img: m.img,
};
});
response(array);
});
},
focus: function (event, ui) {
event.preventDefault();
}
});
// Then redefine the _renderItem for each instance
$('.autocomplete').each(function() {
$(this).data('ui-autocomplete')._renderItem = function (ul, item) {
var $a = $("<div class='ac-m'></div>");
if (item.img) {
$("<span></span>").addClass(item.icon).appendTo($a).append("<img src='" + item.img + "' border='0' class='ac-img' />");
}
$("<span class='ac-title'></span>").text(item.label).appendTo($a);
return $("<li></li>").append($a).appendTo(ul);
};
});

Categories