1

I'm using mapbox-gl-draw to add move-able features to my map. In addition to movability functionality, I am needing rotate/transform -ability functionality for the features akin to Leaflet.Path.Transform.

At current, would my only option to achieve be to create a custom mode?

e.g. something like:

map.on('load', function() {
  Draw.changeMode('transform');
});

I am not able to convert my map and it's features to mapbox-gl-leaflet in order to implement Leaflet.Path.Transform as losing rotation / bearing / pitch support is not an option.

Shawn Goulet
  • 108
  • 1
  • 8
  • Hey Shawn! Are you using draw for its drawing purposes or just to allow movement of features on the map? It may be easier to do a custom solution I can share if you aren't actually drawing features. – tempranova Feb 26 '18 at 00:56
  • Hey Victor! I'm using draw solely for the purpose to allow movement of the mapbox-gl-draw features on the map canvas. Would love to solve that problem however necessary... – Shawn Goulet Feb 26 '18 at 03:38
  • Victor, I should have been more specific in that I need to be able to **move & rotate** the pre-defined geometry features added to the map. – Shawn Goulet Feb 26 '18 at 14:02
  • It'll take me a little bit to write up some help for you. Coming soon:) – tempranova Feb 27 '18 at 06:23

1 Answers1

6

Long answer incoming. (see http://mapster.me/mapbox-gl-draw-rotate-mode and http://npmjs.com/package/mapbox-gl-draw-rotate-mode for some final products, https://github.com/mapstertech/mapbox-gl-draw-rotate-mode)

I've been working on something similar for a custom project, and not using a draw library. My project involves some pretty regularly sized objects, not very complex polygons, so the solution might be too simple for you but it may be the right path. I just have rotate and move.

Doing movement isn't too hard geographically. Here's some help to get you started. A basic JSBin is up at https://jsbin.com/yoropolewo/edit?html,output with some drag functionality (too tired to do rotate too).

First, register the necessary click events to have a dragging event. You can listen on the specific Mapbox layers for a mousedown, then on the whole document for a mousemove and mouseup.

To do individual shape rotation, you need to ensure that you are referring to the right feature. In this example I assume there's just one feature in the source data, but that's probably too simple for most uses, so you have to extrapolate. The source data is what we affect when we setData() later on. There are obviously numerous ways to do what I'm doing here, but I'm trying to be clear.

var currentDragging = false;
var currentDraggingFeature = false;
var currentDraggingType = false;
var firstDragEvent = false;

map.on('mousedown','my-layer-id',function(e) {
    currentDragging = 'my-source-id'; // this must correspond to the source-id of the layer
    currentDraggingFeature = e.features[0]; // you may have to filter this to make sure it's the right feature
    currentDraggingType = 'move'; // rotation or move
    firstDragEvent = map.unproject([e.originalEvent.layerX,e.originalEvent.layerY]);
});
window.addEventListener('mousemove',dragEvent);
window.addEventListener('mouseup',mouseUpEvent);

You will need a function, then, that takes an initial point, a distance, and a rotation, and returns a point back to you. Like this:

Number.prototype.toRad = function() {
    return this * Math.PI / 180;
}

Number.prototype.toDeg = function() {
    return this * 180 / Math.PI;
}

function getPoint(point, brng, dist) { 
    dist = dist / 63.78137; // this number depends on how you calculate the distance
    brng = brng.toRad();

    var lat1 = point.lat.toRad(), lon1 = point.lng.toRad();
    var lat2 = Math.asin(Math.sin(lat1) * Math.cos(dist) +
                      Math.cos(lat1) * Math.sin(dist) * Math.cos(brng));

    var lon2 = lon1 + Math.atan2(Math.sin(brng) * Math.sin(dist) *
                              Math.cos(lat1),
                              Math.cos(dist) - Math.sin(lat1) *
                              Math.sin(lat2));

    if (isNaN(lat2) || isNaN(lon2)) return null;

    return [lon2.toDeg(),lat2.toDeg()];
}

Now, the key is the unproject method in Mapbox GL JS, so you can move between x/y coordinates on the mouse and lng/lat on your map. Then, using the map.getSource().setData() function to set a new geoJSON.

I am turning the x/y into coordinates immediately here but you can do it at any point. Something like the following for moving:

function moveEvent(e) {
    // In the case of move, you are just translating the points based on distance and angle of the drag
    // Exactly how your translate your points here can depend on the shape
    var geoPoint = map.unproject([e.layerX,e.layerY]);
    var xDrag = firstDragEvent.lng - geoPoint.lng;
    var yDrag = firstDragEvent.lat - geoPoint.lat;
    var distanceDrag = Math.sqrt( xDrag*xDrag + yDrag*yDrag );
    var angle = Math.atan2(xDrag, yDrag) * 180 / Math.PI;

    // Once you have this information, you loop over the coordinate points you have and use a function to find a new point for each
    var newFeature = JSON.parse(JSON.stringify(currentDraggingFeature));
        if(newFeature.geometry.type==='Polygon') {
            var newCoordinates = [];
            newFeature.geometry.coordinates.forEach(function(coords) {
                newCoordinates.push(getPoint(coords,distanceDrag,angle));
            });
            newFeature.geometry.coordinates = newCoordinates;
        }
    map.getSource(currentDragging).setData(newFeature);
}

Rotating is a little harder because you want the shape to rotate around a central point, and you need to know the distance of each point to that central point in order to do that. If you have a simple square polygon this calculation would be easy. If not, then using something like this would be helpful (Finding the center of Leaflet polygon?):

var getCentroid2 = function (arr) {
    var twoTimesSignedArea = 0;
    var cxTimes6SignedArea = 0;
    var cyTimes6SignedArea = 0;

    var length = arr.length

    var x = function (i) { return arr[i % length][0] };
    var y = function (i) { return arr[i % length][1] };

    for ( var i = 0; i < arr.length; i++) {
        var twoSA = x(i)*y(i+1) - x(i+1)*y(i);
        twoTimesSignedArea += twoSA;
        cxTimes6SignedArea += (x(i) + x(i+1)) * twoSA;
        cyTimes6SignedArea += (y(i) + y(i+1)) * twoSA;
    }
    var sixSignedArea = 3 * twoTimesSignedArea;
    return [ cxTimes6SignedArea / sixSignedArea, cyTimes6SignedArea / sixSignedArea];        
}

Once you have the ability to know the polygon's center, you're golden:

function rotateEvent(e) {
    // In the case of rotate, we are keeping the same distance from the center but changing the angle

    var findPolygonCenter = findCenter(currentDraggingFeature);
    var geoPoint = map.unproject([e.layerX,e.layerY]);
    var xDistanceFromCenter = findPolygonCenter.lng - geoPoint.lng;
    var yDistanceFromCenter = findPolygonCenter.lat - geoPoint.lat;
    var angle = Math.atan2(xDistanceFromCenter, yDistanceFromCenter) * 180 / Math.PI;

    var newFeature = JSON.parse(JSON.stringify(currentDraggingFeature));
    if(newFeature.geometry.type==='Polygon') {
        var newCoordinates = [];
        newFeature.geometry.coordinates.forEach(function(coords) {

            var xDist = findPolygonCenter.lng - coords[0];
            var yDist = findPolygonCenter.lat - coords[1];
            var distanceFromCenter = Math.sqrt( xDist*xDist + yDist*yDist );
            var rotationFromCenter = Math.atan2(xDist, yDist) * 180 / Math.PI;
            newCoordinates.push(
                getPoint(coords,distanceFromCenter,rotationFromCenter+angle)
            );
        });
        newFeature.geometry.coordinates = newCoordinates;
    }
}

Of course, throughout, ensure that your coordinates are being passed and returned correctly from functions. Some of this code may have incorrect levels of arrays in it. It's very easy to run into bugs with the lat/lng object versus the geoJSON arrays.

I hope the explanation is brief but clear enough, and that you understand logically what we are doing to reorient these points. That's the main point, the exact code is details.

Maybe I should just make a module or fork GL Draw...

tempranova
  • 929
  • 9
  • 13
  • Thank you Victor. Conceptually speaking, I see what you are doing here: it's all about polygon node orientation. I do think that this is worth of a module either in `mapbox-gl-draw` or in `mapbox-gl-js`. `mapbox-gl-draw` is already achieving the **move** functionality, but that the addition here would be the **rotate** functionality. I'm not sure what the use case would be to require not using `mapbox-gl-draw`, requiring `mapbox-gl-js` alone? If there is an argument there, mapbox would possibly want to add to `mapbox-gl-js`? I do think custom polygons is necessary. I'm happy to support. – Shawn Goulet Mar 01 '18 at 14:19
  • Hi Shawn, yes, the rotate is explained above as well. Do you need the JSBin to make it work for you instead of trying with the code above? Also, not sure what you mean re: extending draw or not. I'm not sure if it should be its own module or a fork on GL Draw. Rotate seems a little random for draw? Like if I was going to fork, I should just make a GL Transform library instead (with scaling too)? – tempranova Mar 01 '18 at 15:40
  • Unless there's a JSBin already written, there's no need for you to compose. I will attempt to implement this logic directly in `mapbox-gl-js`. Like I stated in my question, `mapbox-gl-draw` has [custom modes](https://github.com/mapbox/mapbox-gl-draw/blob/master/docs/MODES.md#available-custom-modes) and what I originally was thinking was adding a rotate mode within `mapbox-gl-draw`. Rotate functionality of polygons & lines whether added via `mapbox-gl-draw` or `mapbox-gl-js` makes total sense to me. I can think of many use cases where this is necessary in land use/geo planning applications. – Shawn Goulet Mar 01 '18 at 18:28
  • Cool. I will work on a GL Transform library that does rotation, scaling, and movement. Since Leaflet Draw and Leaflet Transform are separate it might make sense to stay separate. Will update you when I have that in case you're still interested. If you have trouble with rotation, let me know too and I'll JSBin it. – tempranova Mar 01 '18 at 22:11
  • Decided to go with the custom mode route instead. Less work:) – tempranova Mar 02 '18 at 03:12
  • Thought I'd share this custom mode I've mostly finished. https://jsbin.com/buqotajesu/edit?html,output . It still has some bugs. I am atrociously bad at build processes so I'm still trying to understand how to make this a module I can suggest to gl-draw. Hope it helps. – tempranova Mar 04 '18 at 06:32
  • Thanks Victor. I'm just now starting to dig in on tackling this. Are you currently working on adding it as a `mapbox-gl-draw` custom mode or not just yet? – Shawn Goulet Mar 09 '18 at 15:53
  • hi, I am working on adding it to GL Draw, I just need help with the module creation (it doesn't get loaded correctly). I'm planning to have it done next week. In the meantime that latest JSbin should help (https://jsbin.com/buqotajesu/edit?html,output) – tempranova Mar 10 '18 at 19:29
  • Hi Shawn, check out the module. http://mapster.me/mapbox-gl-draw-rotate-mode/ and https://www.npmjs.com/package/mapbox-gl-draw-rotate-mode . I am new to module making so please let me know if you find bugs or problems. – tempranova Mar 14 '18 at 00:09
  • Thanks Victor. I'm getting a TypeError whenever I click outside the draw feature "Cannot read property 'properties' of undefined at t.exports.onMouseDown". Is this code for this module hosted on GitHub or somewhere else where we can chat so as not to keep a running conversation here? – Shawn Goulet Mar 16 '18 at 14:03
  • https://github.com/mapstertech/mapbox-gl-draw-rotate-mode . I think I got that bug too. But hit me up if it's still going or anything else! – tempranova Mar 17 '18 at 05:23