mapbox / mapbox-gl-js

Interactive, thoroughly customizable maps in the browser, powered by vector tiles and WebGL
https://docs.mapbox.com/mapbox-gl-js/
Other
11.05k stars 2.21k forks source link

CustomLayers: Can I listen to the layer's click event #8601

Closed Questionboy closed 5 years ago

Questionboy commented 5 years ago

Hi, I want to listen to the customlayer's click event. Just like this:

map.on('load', function () { map.on('click', 'customlayer', function(e) { console.log('customlayer click'); }); })

however, this doesn't work. Is there any way to get the click event in customlayer?

ansis commented 5 years ago

You need to listen to a click on the map (map.on('click', function(e) { ...) and then do your own calculations of whether there was any feature in the layer at that point

ryanhamley commented 5 years ago

Adding on to @ansis' answer, you can use queryRenderedFeatures inside the click function to determine what layers are visible at that point on the map.

Questionboy commented 5 years ago

thank you very much. @ansis @ryanhamley

Questionboy commented 5 years ago

Hi, as you have said before, I listen to a click on the map and use queryRenderedFeatures inside the click function. But, I cann't get the feature from customlayer. I use the sample of Add a 3D model.

map.on('click', function (e) {
    var bbox = [[e.point.x - 5, e.point.y - 5], [e.point.x + 5, e.point.y + 5]];
    var features = map.queryRenderedFeatures(bbox, { layers: ['3d-model'] });
    console.log(features);
});
ansis commented 5 years ago

Unfortunately map.queryRenderedFeatures(...) can't provide feature querying for custom layers because we have no knowledge of what is being rendered or how. We just provide the canvas and let the layer do whatever it wants.

If you are using threejs you will need to use a threejs api to query the mesh. I think this may be a good starting point: https://threejs.org/docs/#api/en/core/Raycaster You can listen to a click event with map.on('click', ...) and then use the threejs raycaster to check if there is anything at that point.

Questionboy commented 5 years ago

Thank you very much for your patient reply.

N3GIS commented 4 years ago

When I get the model through Api using Three.js getObjectByName, how do I know the LngLat value for this model

ansis commented 4 years ago

@fengfenghao first you need to find the objects MercatorCoordinate by unprojecting the object's position using an inverted version of your matrix. Then you can use mercatorCoordinate.toLngLat() to convert to a lnglat: https://docs.mapbox.com/mapbox-gl-js/api/#mercatorcoordinate#tolnglat

N3GIS commented 4 years ago
<!DOCTYPE html>
<html>
<head>
    <meta charset='utf-8'/>
    <title>Add a 3D model</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.6.0/mapbox-gl.js'></script>
    <link href='https://api.tiles.mapbox.com/mapbox-gl-js/v1.6.0/mapbox-gl.css' rel='stylesheet'/>
    <style>
        body {
            margin: 0;
            padding: 0;
        }

        #map {
            position: absolute;
            top: 0;
            bottom: 0;
            width: 100%;
        }
    </style>
</head>
<body>
<script src='https://unpkg.com/three@0.106.2/build/three.min.js'></script>
<script src="https://unpkg.com/three@0.106.2/examples/js/loaders/GLTFLoader.js"></script>
<div id='map'></div>
<input type="button" name="flyTo" style="position: absolute;left: 10px;bottom: 30px" value="flyTo"
       onclick="focusObject()">
<script>

    var modelString = "{\"metadata\":{\"version\":\"2.0.0\",\"type\":\"Object\",\"generator\":\"zhouzf\"},\"materials\":[{\"color\":\"0xff0000\",\"shininess\":0,\"reflectivity\":0,\"emissive\":\"0x000000\",\"emissiveIntensity\":0,\"uuid\":\"8EA3BB96-0F53-42A7-A524-65BF99F0741A\",\"name\":\"Mat_Red\",\"type\":\"MeshLambertMaterial\",\"side\":0,\"opacity\":1,\"transparent\":false,\"alphaTest\":0},{\"color\":\"0xff0000\",\"shininess\":0,\"reflectivity\":0,\"emissive\":\"0x000000\",\"emissiveIntensity\":0,\"uuid\":\"FE8579A9-919F-467A-8C02-A78B62D968F9\",\"name\":\"Mat_Red\",\"type\":\"MeshLambertMaterial\",\"side\":0,\"opacity\":1,\"transparent\":false,\"alphaTest\":0},{\"color\":\"0xff0000\",\"shininess\":0,\"reflectivity\":0,\"emissive\":\"0x000000\",\"emissiveIntensity\":0,\"uuid\":\"1B5F12BC-D067-400C-A1E4-B1B604392E08\",\"name\":\"Mat_Red\",\"type\":\"MeshLambertMaterial\",\"side\":0,\"opacity\":1,\"transparent\":false,\"alphaTest\":0},{\"color\":\"0xff0000\",\"shininess\":0,\"reflectivity\":0,\"emissive\":\"0x000000\",\"emissiveIntensity\":0,\"uuid\":\"DECA5374-77C2-4693-BE7E-30A907B14F42\",\"name\":\"Mat_Red\",\"type\":\"MeshLambertMaterial\",\"side\":0,\"opacity\":1,\"transparent\":false,\"alphaTest\":0},{\"color\":\"0xff0000\",\"shininess\":0,\"reflectivity\":0,\"emissive\":\"0x000000\",\"emissiveIntensity\":0,\"uuid\":\"6C6D922D-2927-4D18-B041-DF33FA8A6380\",\"name\":\"Mat_Red\",\"type\":\"MeshLambertMaterial\",\"side\":0,\"opacity\":1,\"transparent\":false,\"alphaTest\":0}],\"geometries\":[{\"data\":{\"index\":{\"itemSize\":1,\"type\":\"Uint16Array\",\"array\":[0,3,2,0,1,3,8,5,4,8,9,5,10,7,6,10,11,7,12,14,13,12,15,14,16,18,17,16,19,18,20,22,21,20,23,22]},\"attributes\":{\"position\":{\"itemSize\":3,\"type\":\"Float32Array\",\"array\":[0.5,-0.5,-0.5,-0.5,-0.5,-0.5,0.5,0.5,-0.5,-0.5,0.5,-0.5,0.5,0.5,0.5,-0.5,0.5,0.5,0.5,-0.5,0.5,-0.5,-0.5,0.5,0.5,0.5,-0.5,-0.5,0.5,-0.5,0.5,0.5,0.5,-0.5,0.5,0.5,0.5,-0.5,0.5,0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,0.5,-0.5,-0.5,-0.5,-0.5,0.5,-0.5,-0.5,0.5,0.5,-0.5,-0.5,0.5,0.5,-0.5,0.5,0.5,0.5,0.5,0.5,0.5,-0.5,0.5,-0.5,-0.5]},\"normal\":{\"itemSize\":3,\"type\":\"Float32Array\",\"array\":[0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,1,0,0,1,0,0,0,1,0,0,1,0,1,0,0,1,0,0,0,1,0,0,1,0,-1,0,0,-1,0,0,-1,0,0,-1,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,1,0,0,1,0,0,1,0,0,1,0,0]},\"color\":{\"itemSize\":3,\"type\":\"Float32Array\",\"array\":[]},\"uv\":{\"itemSize\":2,\"type\":\"Float32Array\",\"array\":[0,0,1,0,0,1,1,1,0,1,1,1,0,1,1,1,0,0,1,0,0,0,1,0,0,0,0,1,1,1,1,0,0,0,0,1,1,1,1,0,0,0,0,1,1,1,1,0]},\"uv2\":{\"itemSize\":2,\"type\":\"Float32Array\",\"array\":[0.691,0.3472,0.9967,0.3472,0.691,0.6528,0.9967,0.6528,0.3097,0.3472,0.0041,0.3472,0.3472,0.6528,0.6528,0.6528,0.3097,0.6528,0.0041,0.6528,0.3472,0.3472,0.6528,0.3472,0.3472,0.0041,0.3472,0.3097,0.6528,0.3097,0.6528,0.0041,0.6903,0.3097,0.9959,0.3097,0.9959,0.0041,0.6903,0.0041,0.3097,0.0041,0.0041,0.0041,0.0041,0.3097,0.3097,0.3097]}}},\"uuid\":\"02365fa8-2a4d-4039-abb5-99653eaa86cc\",\"type\":\"BufferGeometry\"}],\"object\":{\"visible\":true,\"castShadow\":false,\"receiveShadow\":false,\"children\":[{\"visible\":true,\"geometry\":\"02365fa8-2a4d-4039-abb5-99653eaa86cc\",\"material\":\"8EA3BB96-0F53-42A7-A524-65BF99F0741A\",\"castShadow\":false,\"receiveShadow\":false,\"uuid\":\"0280e927-cab1-4c75-a895-e2312bef42c5\",\"name\":\"Cube_0\",\"type\":\"Mesh\",\"matrix\":[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]},{\"visible\":true,\"geometry\":\"02365fa8-2a4d-4039-abb5-99653eaa86cc\",\"material\":\"FE8579A9-919F-467A-8C02-A78B62D968F9\",\"castShadow\":false,\"receiveShadow\":false,\"uuid\":\"ab0b58e8-ea28-45d1-813d-856082ebc59c\",\"name\":\"Cube_1\",\"type\":\"Mesh\",\"matrix\":[1,0,0,0,0,1,0,0,0,0,1,0,-200,0,-200,1]},{\"visible\":true,\"geometry\":\"02365fa8-2a4d-4039-abb5-99653eaa86cc\",\"material\":\"1B5F12BC-D067-400C-A1E4-B1B604392E08\",\"castShadow\":false,\"receiveShadow\":false,\"uuid\":\"91d183a6-a938-4039-9210-3574d5dd8bf7\",\"name\":\"Cube_2\",\"type\":\"Mesh\",\"matrix\":[1,0,0,0,0,1,0,0,0,0,1,0,200,0,-200,1]},{\"visible\":true,\"geometry\":\"02365fa8-2a4d-4039-abb5-99653eaa86cc\",\"material\":\"DECA5374-77C2-4693-BE7E-30A907B14F42\",\"castShadow\":false,\"receiveShadow\":false,\"uuid\":\"4d8e622a-42eb-4404-8366-a3628750789a\",\"name\":\"Cube_3\",\"type\":\"Mesh\",\"matrix\":[1,0,0,0,0,1,0,0,0,0,1,0,200,0,200,1]},{\"visible\":true,\"geometry\":\"02365fa8-2a4d-4039-abb5-99653eaa86cc\",\"material\":\"6C6D922D-2927-4D18-B041-DF33FA8A6380\",\"castShadow\":false,\"receiveShadow\":false,\"uuid\":\"b1c69cc7-c075-4688-b5a8-45d02a2b3cce\",\"name\":\"Cube_4\",\"type\":\"Mesh\",\"matrix\":[1,0,0,0,0,1,0,0,0,0,1,0,-200,0,200,1]}],\"uuid\":\"bd9f1b4b-1887-44df-8f6c-723af23507e1\",\"name\":\"Root\",\"type\":\"Object3D\",\"matrix\":[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]},\"images\":[],\"textures\":[]}"
    mapboxgl.accessToken = 'pk.eyJ1IjoiaGFtYnVnZXJkZXZlbG9wIiwiYSI6ImNqNXJtZjF4ZzB3em4yd21pZmVqbHlleDAifQ.I9eqVjtotz7jaU7XcJm9pQ';
    var map = window.map = new mapboxgl.Map({
        container: 'map',
        style: 'mapbox://styles/mapbox/light-v10',
        zoom: 18,
        center: [148.9819, -35.3981],
        pitch: 60,
        antialias: true // create the gl context with MSAA antialiasing, so custom layers are antialiased
    });

    // parameters to ensure the model is georeferenced correctly on the map
    var modelOrigin = [148.98190, -35.39847];
    var modelAltitude = 0;
    var modelRotate = [Math.PI / 2, 0, 0];

    var modelAsMercatorCoordinate = mapboxgl.MercatorCoordinate.fromLngLat(modelOrigin, modelAltitude);

    // transformation parameters to position, rotate and scale the 3D model onto the map
    var modelTransform = {
        translateX: modelAsMercatorCoordinate.x,
        translateY: modelAsMercatorCoordinate.y,
        translateZ: modelAsMercatorCoordinate.z,
        rotateX: modelRotate[0],
        rotateY: modelRotate[1],
        rotateZ: modelRotate[2],
        /* Since our 3D model is in real world meters, a scale transform needs to be
        * applied since the CustomLayerInterface expects units in MercatorCoordinates.
        */
        scale: modelAsMercatorCoordinate.meterInMercatorCoordinateUnits()
    };

    var THREE = window.THREE;
    var scene, camera;
    // configuration of the custom layer for a 3D model per the CustomLayerInterface
    var customLayer = {
        id: '3d-model',
        type: 'custom',
        renderingMode: '3d',
        onAdd: function (map, gl) {
            camera = new THREE.Camera();
            scene = new THREE.Scene();

            // create two three.js lights to illuminate the model
            var directionalLight = new THREE.DirectionalLight(0xffffff);
            directionalLight.position.set(0, -70, 100).normalize();
            scene.add(directionalLight);

            var directionalLight2 = new THREE.DirectionalLight(0xffffff);
            directionalLight2.position.set(0, 70, 100).normalize();
            scene.add(directionalLight2);

            this.map = map;

            // use the Mapbox GL JS map canvas for three.js
            this.renderer = new THREE.WebGLRenderer({
                canvas: map.getCanvas(),
                context: gl,
                antialias: true
            });

            this.renderer.autoClear = false;

            //use three objcetLoader loading models
            var loader = new THREE.ObjectLoader();
            loader.parse(JSON.parse(modelString), function (obj) {
                scene.add(obj);
                for (var i = 0; i < obj.children.length; i++) {
                    console.log(obj.children[i].name);
                }
            });
        },
        render: function (gl, matrix) {
            var rotationX = new THREE.Matrix4().makeRotationAxis(new THREE.Vector3(1, 0, 0), modelTransform.rotateX);
            var rotationY = new THREE.Matrix4().makeRotationAxis(new THREE.Vector3(0, 1, 0), modelTransform.rotateY);
            var rotationZ = new THREE.Matrix4().makeRotationAxis(new THREE.Vector3(0, 0, 1), modelTransform.rotateZ);

            var m = new THREE.Matrix4().fromArray(matrix);
            var l = new THREE.Matrix4().makeTranslation(modelTransform.translateX, modelTransform.translateY, modelTransform.translateZ)
                .scale(new THREE.Vector3(modelTransform.scale, -modelTransform.scale, modelTransform.scale))
                .multiply(rotationX)
                .multiply(rotationY)
                .multiply(rotationZ);

            camera.projectionMatrix = m.multiply(l);
            this.renderer.state.reset();
            this.renderer.render(scene, camera);
            this.map.triggerRepaint();
        }
    };

    map.on('style.load', function () {
        map.addLayer(customLayer, 'waterway-label');
    });

    map.on('mouseup', function (e) {
        console.log(JSON.stringify(e.point) + " : " + JSON.stringify(e.lngLat));
    });

    function focusObject() {
        //Cube_1  lngLat:{"lng":148.97969602816397,"lat":-35.400267269123674}
        var findObj = scene.getObjectByName("Cube_1");
        console.log(findObj);
        //How do I get the LngLat value of findObj, and I don't know how to get the MercatorCoordinate value, can you write this code for me

        map.flyTo({center: [148.97969602816397, -35.400267269123674], zoom: 19});
    }
</script>

</body>
</html>

How do I get the LngLat value of findObj, and I don't know how to get the MercatorCoordinate value, can you write this code for me

len-art commented 4 years ago

Hi. I decided to ask my question here since I'm having similar problems to @Questionboy. I'm by no means a WebGL expert, so I apologize in advance if anything of this might sound stupid. I have been researching the subject thoroughly for a couple of days now, though, and I hope it makes at least some sense.

I'm trying to implement a custom layer that renders a bunch of stuff (polygons or 2D textures, not that far yet) on different latLng locations. These models must, of course, be clickable. So I'm trying to implement some sort of click detection, and from what I've read raycasting seems a proper solution. Oh, and I'm not using any graphics engine, just vanilla WebGL. I'm having problems with understanding how to ray cast on a click. I can obviously convert a click (or model's vertices) location to MercatorCoordinate, but don't fully understand how to use that for detecting a collision.

The easiest solution seems to be to just check if click location is close enough to the locations (in screen coordinates), but this seems hacky and imprecise.

I know this is not a place for seeking help with such questions, and I'll be grateful if anybody can even only push me in the right direction.

jscastro76 commented 4 years ago

@Len-art, @Questionboy I know this is a quite old question, but if you still interested in this kind of functionality, I think what you need can be found in this Threebox fork.

len-art commented 4 years ago

@jscastro76 we ended up with actual raycasting and it worked great. Unfortunately, other Mapbox limitations (not having access to the transformation matrix or however it's called, it's been a while) made the whole thing too hacky in the end. Thanks for the link, I might revisit the problem using the fork.

QSafariallahkheili commented 4 months ago

Accessing and calling the pick() method in the layer's scene object may solve the issue:

map.on('click', (e) => { const canvasCoords = [e.point.x, e.point.y]; const picked = map.getLayer("customlayer").implementation.scene.pick(canvasCoords[0], canvasCoords[1]); console.log(picked.hit) });

In which the hit property returns a Boolean value indicating if the cursor hit the layer.