Leaflet / Leaflet.draw

Vector drawing and editing plugin for Leaflet
https://leaflet.github.io/Leaflet.draw/docs/leaflet-draw-latest.html
MIT License
1.98k stars 994 forks source link

Question Urgent need HELP very badly : How to append new point (marker) to existing line ? #524

Open help53 opened 8 years ago

help53 commented 8 years ago

Hello everybody,

I am a debutant developper and it is first time i am using leaflet. I need to do be able to create new markers each time the user click on the map and to draw a line binding the all the markers. Then i need to be able to add a new marker between two markers, thus to draw the line with the new marker included.

something like this :

A ----------------------- B

Here there are two markers A and B on the map. Both are binded with a line .

I need to add a marker C between A and B and to draw a line between binding all three markers, like this :

A-------------------------------C-------------------------------------B

But i want to be able to do this when the user is clicking.

I succeeded in creating markers on each click of the user and draw a line between all the markers, (i used the event onClick(), but now i need a help from you all to add a marker between two binded markers .

Please how do i do this, i really your help with this as i am making this for a school project and i need to submit it.

thanks

Waiting eagerly for your help .... Help53

ddproxy commented 8 years ago

Hey @help53, would you happen to have any jsfiddle code somewhere I could see where you are at with this?

help53 commented 8 years ago

hello,

i did try to do a jsfiddle but the map isnt appearing somehow, i can copy past my code here if its alright for you.

More information : i am using cake php framework

help53 commented 8 years ago

here is my js code:

$(document).ready(function() {
        var markers = new Array();
        var map = L.map('map').setView([48,8], 5);
        var marker;
        var tuileUrl = 'http://{s}.tile.osm.org/{z}/{x}/{y}.png';
        var attrib='Map data © <a href="http://openstreetmap.org">OpenStreetMap</a> contributors';
        var osm = L.tileLayer(tuileUrl, {
            minZoom: 5, 
            maxZoom: 17,
            attribution: attrib
        });
        osm.addTo(map);

        map.on('click', function(e) {

            marker = new L.Marker(e.latlng, {draggable:true});
            map.addLayer(marker);
            markers.push(marker);
            var longMarker = markers.length;
            var test = new Array();

            // create a red polyline from an array of LatLng points
            if (markers.length > 1 ){
            for (i = 0; i < markers.length; i++) { 
                test.push(markers[i].getLatLng()) ;
            }
            var polyline = L.polyline(test, {color: 'red', clickable: 'true'}).addTo(map);
            }
        });

    });

this is my html code :

<div id="map">

</div>
ddproxy commented 8 years ago

@help53 http://jsfiddle.net/ddproxy/fcjfty7a/1/ Is with your above code, I'm looking at how you have it set up and will reply with my findings ;)

help53 commented 8 years ago

ok thank you, as i explained, it is quite important for my diploma.

thanks :)

ddproxy commented 8 years ago

Okay, here's what I've got so far.

You are adding a marker for each click. Each marker is added to an array. With each click, you draw a line between all markers in the array. If a marker is moved, the line is not redrawn. When a marker is added again, it's appended to the array and the line is redrawn.

So, now the questions - Is the line supposed to always follow the markers or is a new line supposed to be drawn with each new marker?

My first read of the question - I assumed you would want the polyline to be 'bound' to the marker's points so you have an 'array' of markers and a single polyline underneath it that has a new 'point' added to it with each new marker.

Now what's going on so far in your demo is each new marker adds a new poly-line. If you were to save your poly-lines to an array, you would have N poly lines for N markers. If you wanted to have one poly-line in total, you would have to move the var polyline declaration outside of the onClick scope so you can remove and re-draw that polyline.

var polyline = null;
map.on('click', function(e) {
  marker = new L.Marker(e.latlng, {draggable:true});
  map.addLayer(marker);
  markers.push(marker);
  var longMarker = markers.length;
  var test = new Array();

  // create a red polyline from an array of LatLng points
  if (markers.length > 1 ){
    for (i = 0; i < markers.length; i++) { 
      test.push(markers[i].getLatLng()) ;
    }
map.removeLayer(polyline);
polyline = L.polyline(test, {color: 'red', clickable: 'true'}).addTo(map);
  }
});

http://jsfiddle.net/ddproxy/fcjfty7a/2/

Now being extra fancy, we could just modify the existing polyline in place instead of creating a new polyline object. Then we can tell it to redraw with it's new coordinates and tell the marker to update the coordinate within the polyline when it's been moved.

var polyline = L.polyline([], {color: 'red', clickable: 'true'}).addTo(map);
map.on('click', function(e) {
  marker = new L.Marker(e.latlng, {draggable:true});
  map.addLayer(marker);
  markers.push(marker);
  polyline.addLatLng(marker.getLatLng());
  // Length - 1 is current index since arrays start with 0
  marker._polylineIndex = polyline.getLatLngs().length -1;
  polyline.redraw();

  marker.on('dragend', function(e) {
    polyline.spliceLatLngs(this._polylineIndex, 1, this.getLatLng());
    // Redraw polyline!
    polyline.redraw();
  })
});

http://jsfiddle.net/ddproxy/fcjfty7a/3/

Now that this is cleaned up, we want to be able to inject a marker, lat lng, onto the existing polyline. The function here is very similar to just clicking on the map so we'll make an anonymous function to handle the creation of the marker.


var polyline = L.polyline([], {color: 'red', clickable: 'true'}).addTo(map);
var makeMarkerAndPolyline = function(latLng) {
  marker = new L.Marker(latLng, {draggable:true});
  map.addLayer(marker);
  markers.push(marker);
  polyline.addLatLng(marker.getLatLng());
  // Length - 1 is current index since arrays start with 0
  marker._polylineIndex = polyline.getLatLngs().length -1;
  polyline.redraw();

  // Marker events
  marker.on('dragend', function(e) {
    polyline.spliceLatLngs(this._polylineIndex, 1, this.getLatLng());
    // Redraw polyline!
    polyline.redraw();
  });

  return marker;
}
// Events
polyline.on('click', function(e) {
    var marker = makeMarkerAndPolyline(e.latlng);
})
map.on('click', function(e) {
    var marker = makeMarkerAndPolyline(e.latlng);
});

http://jsfiddle.net/ddproxy/fcjfty7a/4/

So quick recap of the progress, we've brought the polyline to a single-object, anonymized the marker creation and adding to the polyline so it can be used in two events, drawn up an event for the marker drag-end to redraw the polyline after splicing the new latLngs' into the internal array of points for the polyline.

We currently don't have the polyline being spliced between markers to add an interjecting point. We have this anonymous function handling the majority of the marker logic, however. So we need to add an additional (optional) parameter to the function to define the splice index to inject the new marker (and subsequent polyline latlng).

It's important to note here that we have to separate arrays we are dealing with - an array of markers and an array of points on the polyline. The indexes we have saved to the markers are associated to both markers and polyline._latlngs at this point since they are the same length (and same points). If we were to determine the intermediary index point for the new marker when clicking the polyline, we can pass the spliced index into the anonymous function to properly inject the marker and the latlng into both arrays. We'll get to that in a moment, but we also need to realize the entire length and index associations for the markers and lat lngs' would change at this point, taking your original explanation into effect, but in terms of index...

0====1===========2

Injecting a new point between 1 and 2...

0====1=====2=====3

But we haven't updated the original marker 2, so the markers in the marker array think it's...

0====1=====2=====2

We can fix this by walking down the array and re-indexing the markers to be properly associated, or because we have just injected a point - we can just add one to each marker._polylineIndex when we walk the array.

We still need to determine what the index of this point needs to be though! Herin lies a problem, if we do strict distance to each point/marker and fetch the saved _polylineIndex, we may be getting an index item ahead of target marker or be selecting a marker that's not part of the existing line-segment (close but not intended).

Instead, we can walk through the points/markers and see if the e.latlng lies along the segment of the the last two points. If success, we can select the previous marker index and use it's index as our injection point. But we have a problem with this - our line is 4px's wide and clicking on the line may not create an exact match for our point-checking-algorithm. It would be easier to use the L.pointToSegmentDistance(p, p1, p2) to check the distance between our clicked point on segment and the actual segment.

Example: http://jsfiddle.net/ddproxy/fcjfty7a/5/

Now this is all using Leaflet (not Leaflet.draw). Leaflet.draw's edit function injects a proto-point on the line segments you can drag-drop to create a new point on that line. It's not bound to markers, but you could save a new marker on each point and use the point to modify the marker (or vice versa, save the point and update the polyline) to use Leaflet.draw. Alternatively, you could just use the polyline as it is without a marker.

help53 commented 8 years ago

Hello,

thanks for your help really, i don't think would ve been able to create this without your help. I find programming quite difficult ...

I still ve questions though :

something like :+1:

A=====B=====C=====D=====A

accrobat_carte

If i created Three Markers ABC, having respectively the numbers 1-2-3, and i created Two markers D-E, D between A-B and E between B-C , i would like the numbers to become : A-D-B-E-C (1,2,3,4,5)

A====B=====C 1====2=====3 ----------------------------------> ve three markers

A====D====B====E====C 1====2====3====4=====5

--------------------------------------> the markers ve the following numbers.

I tried to put the folowing code :+1:

marker.bindPopup(marker._polylineIndex.toString()).openPopup();

Like this :

map.on('click', function(e) {
  var marker = makeMarkerAndPolyline(e.latlng, null);
  marker.bindPopup(marker._polylineIndex.toString()).openPopup();
});

and like this :

var makeMarkerAndPolyline = function(latLng, subIndex) {
    marker = new L.Marker(latLng, {
      draggable: true
    });
    map.addLayer(marker);
    markers.push(marker);

    // Reassign subIndex if null
    if (!subIndex) {
      subIndex = polyline.getLatLngs().length;
    }
    polyline.spliceLatLngs(subIndex, 0, marker.getLatLng());
    marker._polylineIndex = subIndex;
    marker.bindPopup(marker._polylineIndex.toString()).openPopup();
    polyline.redraw();

    // Marker events
    marker.on('dragend', function(e) {
      polyline.spliceLatLngs(this._polylineIndex, 1, this.getLatLng());
      // Redraw polyline!
      polyline.redraw();
    });

    return marker;
  }

But that didn't work ...

And thanks for helping out and specially explaining so well :)

waiting for your help

ddproxy commented 8 years ago

You have the right idea, just needs to be fleshed out a bit.

Adding a popup and setting new content for the popup when updating indices... http://jsfiddle.net/ddproxy/fcjfty7a/7/

First I'm binding a basic popup to the marker with the shortcut, then I add a .popup declaration to the marker so I'm not accessing the popup via _popup. Then just setting the content to the appropriate string.

We also add the setContent method to the index update method so they propagate properly. The setContent accepts a string so when we update our polylineIndex, it does not automatically change the popup content.

To allow a user to use a context menu on a marker, we'll use the .on('contextmenu', callback()) We need to define the popup beforehand since we are going to call it dynamically from the marker. We also use var that = this; to hand the marker scope off to the next piece of goodness - actual button-press detection.

 $(largerScopePopup._contentNode).find('button').on('click', function(e) {
    polyline.spliceLatLngs(that._polylineIndex, 1);
    pullMarkers(that._polylineIndex);
    polyline.redraw();
    map.removeLayer(that);
    markers.splice(markers.indexOf(that), 1);
    map.closePopup();
});

This is jquery method to 'find' the button in the popup we predefined and give it a javascript function (on click) to do our deletion magic...

http://jsfiddle.net/ddproxy/fcjfty7a/9/

To close the loop, we can just a polygon instead of a polyline. We have several options here to do this. Either we use a popup again to provide a button to close the loop, just attach a click event to the first marker to close the loop, or have an external button somewhere to close the loop.

I'm going to write this to assign a context menu to the map that provides a button to close the loop. The logic for swapping out the polyline to a polygon is straightforward - copy out the old polyline, add the options from that polyline and latLngs to the new polygon and swap them out via layer controls.

map.on('contextmenu', function (e) {
    var loopPopup = L.popup().setContent('<button>Close Loop</button>')
        .setLatLng(e.latlng).openOn(map);
    $(loopPopup._contentNode).find('button').on('click', function(e) {
        var oldPolyline = polyline;
        polyline = new L.polygon(polyline.getLatLngs(),
        polyline.options
    );
    map.removeLayer(oldPolyline);
    map.addLayer(polyline);
    polyline.redraw();
    map.closePopup();
});

http://jsfiddle.net/ddproxy/fcjfty7a/10/

I know these are all pretty ugly on the UI side - the code will probably leak memory and there are plenty of events being wired up that may cause problems down the road. What I'm getting at is this is definitely not production code.

ddproxy commented 8 years ago

@help53 By the way, Cake PHP is a server-side framework whilst Leaflet is (primarily) a front-end or browser scripting language. I figure you are either putting this javascript in a script tag or importing it to the page from a resource location. Although knowing you are using Cake PHP behind the scenes might be important, it doesn't help much here ;)

I'm actually a PHP developer too, so feel free to hit me up on Skype (ddproxy) or https://gitter.im/ (also ddproxy). I'm also capable of hopping on most other forms of chat messenger if you'd like help in either javascript or PHP.

help53 commented 8 years ago

hello ,

i checked the code it is really impressive, i tried the close loop button its working but i wanted to be able to remove the loop , so i changed a bit the function map.on(contextmenu....) as below.

Actually i declared a boolean called loopclosed = false in the begining. then i check everytime if loop is closed or no, if its opened then the button close appears, if the loop is closed a button open loop is shown. That is apparently working but i would like your expertise on that piece of code i producted.

map.on('contextmenu', function (e) {

    if(loopclosed == false){
        var loopPopup = L.popup().setContent('<button>Close Loop</button>')
            .setLatLng(e.latlng).openOn(map);
        $(loopPopup._contentNode).find('button').on('click',
        function(e) {
            var oldPolyline = polyline;
            alert(polyline.getLatLngs());
            polyline = new L.polygon(polyline.getLatLngs(),
                polyline.options);
            map.removeLayer(oldPolyline);
            map.addLayer(polyline);
            alert(polyline.getLatLngs());
            polyline.redraw();
            map.closePopup();
            loopclosed = true; 
        });
    }

    if(loopclosed){
        loopPopup = L.popup().setContent('<button>Open Loop</button>')
            .setLatLng(e.latlng).openOn(map);
        $(loopPopup._contentNode).find('button').on('click',
        function(e) {
        var oldPolyline = polyline;
        polyline = new L.polyline(polyline.getLatLngs(),
            polyline.options);
        map.removeLayer(oldPolyline);
        map.addLayer(polyline);
        polyline.redraw();
        map.closePopup();
        loopclosed = false; 
        });
    } 

}); 

However since i changed this code when i try to create a marker between two markers, it doesn't insert that marker on the drawn polyline but creats it after the last marker created.

here is full code :

$(document).ready(function() {
var markers = new Array();
var map = L.map('map').setView([48, 8], 5);
var marker;
var loopclosed = false;//checks if loopclosed

var tileUrl = 'http://{s}.tile.osm.org/{z}/{x}/{y}.png';
var osm = L.tileLayer(tileUrl, {
  minZoom: 5,
  maxZoom: 17
});

osm.addTo(map);
var largerScopePopup = L.popup().setContent('<button>Remove 

Marker</button>'); 
var polyline = L.polyline([], {
  color: 'red',
  clickable: 'true'
}).addTo(map);

map.on('contextmenu', function (e) {
    if(loopclosed == false){
        var loopPopup = L.popup().setContent('<button>Close Loop</button>')
            .setLatLng(e.latlng).openOn(map);
        $(loopPopup._contentNode).find('button').on('click',
        function(e) {
            var oldPolyline = polyline;
            alert(polyline.getLatLngs());
            polyline = new L.polygon(polyline.getLatLngs(),
                polyline.options);
            map.removeLayer(oldPolyline);
            map.addLayer(polyline);
            alert(polyline.getLatLngs());
            polyline.redraw();
            map.closePopup();
            loopclosed = true; 
        });
    }

    if(loopclosed){
        loopPopup = L.popup().setContent('<button>Open Loop</button>')
            .setLatLng(e.latlng).openOn(map);
        $(loopPopup._contentNode).find('button').on('click',
        function(e) {
        var oldPolyline = polyline;
        polyline = new L.polyline(polyline.getLatLngs(),
            polyline.options);
        map.removeLayer(oldPolyline);
        map.addLayer(polyline);
        polyline.redraw();
        map.closePopup();
        loopclosed = false; 
        });
    }  
}); 

var makeMarkerAndPolyline = function(latLng, subIndex) {
    marker = new L.Marker(latLng, {
        clickable: true,
      draggable: true
    });
    map.addLayer(marker);
    markers.push(marker);

    // Reassign subIndex if null
    if (!subIndex) {
      subIndex = polyline.getLatLngs().length;
    }
    polyline.spliceLatLngs(subIndex, 0, marker.getLatLng());
    marker._polylineIndex = subIndex;
    polyline.redraw();

    marker.bindPopup("Popup");
    marker.popup = marker._popup;
    //marker.popup.setContent(marker._polylineIndex.toString());
    pop = marker._polylineIndex + 1;
    marker.popup.setContent(pop.toString());

    // Marker events
    marker.on('dragend', function(e) {
      polyline.spliceLatLngs(this._polylineIndex, 1, this.getLatLng());
      // Redraw polyline!
      polyline.redraw();
    }).on('contextmenu', function(e) {
        var that = this;
        largerScopePopup.setLatLng(e.latlng).openOn(map);
      $(largerScopePopup._contentNode).find('button').on('click',
        function(e) {
            polyline.spliceLatLngs(that._polylineIndex, 1);
            pullMarkers(that._polylineIndex);
          polyline.redraw();
          map.removeLayer(that);
                    markers.splice(markers.indexOf(that), 1);
                    map.closePopup();
        });
    });

    return marker;
  }
  // Events
polyline.on('click', function(e) {
  var previousMarker = null;
  var matchedMarker = null;
  var closestMatch = null;
  markers.forEach(function(marker) {
    if (!previousMarker) {
      previousMarker = marker;
      return; // continue does not work in foreach
    }
    var startLatLng = previousMarker.getLatLng();
    var endLatLng = marker.getLatLng();
    var protoDistance = L.LineUtil.pointToSegmentDistance(
      latLngToXY(e.latlng),
      latLngToXY(startLatLng),
      latLngToXY(endLatLng)
    );
    if (!closestMatch || protoDistance < closestMatch) {
        closestMatch = protoDistance;
      matchedMarker = previousMarker;
    }
    previousMarker = marker;
  });
  var indexToInject = matchedMarker._polylineIndex + 1;
  pushMarkers(indexToInject)
  var currentMarker = makeMarkerAndPolyline(e.latlng, indexToInject);
})
map.on('click', function(e) {
  var marker = makeMarkerAndPolyline(e.latlng, null);
});

function latLngToXY(latLng) {
    return {x: latLng.lng, y: latLng.lat};
}

function pushMarkers(index) {
    markers.forEach(function(marker) {
    if (marker._polylineIndex >= index) {
        marker._polylineIndex++;
        pop = marker._polylineIndex + 1;
        marker.popup.setContent(pop.toString());
        //marker.popup.setContent(marker._polylineIndex.toString());
    }
  });
}

function pullMarkers(index) {
    markers.forEach(function(marker) {
    if (marker._polylineIndex >= index) {
        marker._polylineIndex--;
        pop = marker._polylineIndex + 1;
        marker.popup.setContent(pop.toString());
        //marker.popup.setContent(marker._polylineIndex.toString());
    }
  });
}
    });

Please can you help in that matter ?

ddproxy commented 8 years ago

http://jsfiddle.net/ddproxy/fcjfty7a/12/

We lose the .on(click binding to the polyline when we change objects. So I've made the binding an anonymous method and bind it after changing the polyline object.

I've tweaked the popup logic to use the "instanceof" for polyline to determine whether it's a polygon or not. :)

help53 commented 8 years ago

Hello @ddproxy ,

thanks for your expertise, i am Learning lots of programming with you. Actually i noticed something, when i add a lot of markers (between two markers) and remove some markers , after one point, the markers i add are not added between the two markers i want but it is appended to the last marker created .

In the picture the black dot is where i want to add a marker , the marker that is half circuled is the one i added last 🎱

marker_before

This is the result after i added the marker between those two markers, as you can see there is line between the previously added marker and the one i added just now. :

marker_added

I dragged the marker so that you can how it was added :

marker_added_dragged

How to fix this bug ? i tried to understand but to no avail :(

ddproxy commented 8 years ago

This has to deal with the order of the indices in both the marker and the polyline points index. I'll take a look at it later today. It's one of two operations are not happening - the index push or index pull.

ddproxy commented 8 years ago

I think this is a bug related to the order of the markers (in the markers array) and the actual order of the points in the polygon vs the subindex assigned to the marker. I bet you could flip this process around and append points to the polylines (or between points in the polyline/polygon) and bind a marker to the point as a handler.

I implore you to try this out yourself ;) Let me see your progress and I'll help from there.

help53 commented 8 years ago

hello @ddproxy ,

i tried to understand wht u tried to explained but i didn't , wht do you mean by

append points to the polylines (or between points in the polyline/polygon) and bind a marker to the point as a handler

??

help53 commented 8 years ago

hello @ddproxy ,

i tried to do wht you did but still doens't work , so could you help plz ! i really need it badly for my project.

thx