Unite paths, edge cases - paperjs

I was wondering why is unite of a path composed of only one point, with an other path of 2 points returns nothing!
An other case is unite of a two points' path and any other geometrical form , it returns the last item and ignores the 2 points' path.
Thank you!
var carre = new Path({
segments: [[30, 75], [30, 25], [80, 25], [80, 75]],
strokeColor: 'black',
closed: true
});
// Select the path, so we can see its handles:
carre.fullySelected = true;
// Create a copy of the path and move it 100pt to the right:
var ligne = new Path({
segments: [[30, 75], [41, 25]],
strokeColor: 'black',
closed: false
});
ligne.fullySelected = true;
ligne.position.x += 100;
var uniteres = carre.unite(ligne);
uniteres.fullySelected = true;
uniteres.position.x += 200;

You are right to find this weird.
In fact I would say that this is a bug in the boolean operation algorithm, which you can report here: https://github.com/paperjs/paper.js/issues

Related

How to join connected sub-paths by eliminate useless point with PaperJS?

I have a path that draw a circle whose origin is in the "west" side, then I split by removing the top and bottom. Then I get three sub-paths:
Top-left 1/4 circle
Right half circle
Bottom-left 1/4 circle
But even visually 1 and 3 looks like a flipped 2, 1 and 3 are actually two sub-paths. How do I optimize this? I've tried smooth(), flatten() and simplify() and all not work.
Here is the sketch.
Based on your simplified case, you just have to build a new path composed of all your sub paths segments.
In order to optimize the resulting path a bit, you can skip the first segment of path B and only keep its handle out, since it's the same than path A last segment.
Depending on your use case, you could also, with the same logic, skip the last segment of path B since it's the same than path A first segment and make sure that the resulting path is set to closed.
Here is a sketch demonstrating a possible implementation.
const compoundPath = project.importJSON(
['CompoundPath', { 'applyMatrix': true, 'children': [['Path', { 'applyMatrix': true, 'segments': [[50, 700], [0, 700], [0, 600], [50, 600]] }], ['Path', { 'applyMatrix': true, 'segments': [[50, 600], [100, 600], [100, 700], [50, 700]] }]] }]
);
compoundPath.strokeColor = 'black';
project.activeLayer.addChild(compoundPath);
const subPaths = [];
compoundPath.children.forEach((child, i) => {
subPaths.push(
child
.clone()
.translate(0, 150)
.addTo(project.activeLayer)
);
});
const assembledPath = assembleSubPaths(subPaths);
assembledPath.strokeColor = 'black';
function assembleSubPaths(subPaths) {
const path = new Path();
subPaths.forEach((subPath) => {
subPath.segments.forEach((segment, segmentIndex) => {
const isFirstSegment = segmentIndex === 0;
if (path.segments.length === 0 || !isFirstSegment) {
path.add(segment);
} else {
path.lastSegment.handleOut = segment.handleOut;
}
});
subPath.remove();
});
return path;
}

How to create a hole on a Path in Paper.js?

I want to get a result like this while the white circle is being actually a punch:
However, I'm getting the following result when I follow the boolean operation example:
// this works okay
var via = outer.exclude(hole)
project.activeLayer.addChild(via)
// this works weird
var drilledY = y.exclude(hole)
project.activeLayer.addChild(drilledY)
Here the only problem seems to be creating the hole inside the Path. How can I create a hole in the path?
I don't think you can get the result you want using Path.Line.
Punching through implies that you want to remove some internal area, which an open Path such as Path.Line lacks.
So what you can do is the following:
Replace those thick Lines with Path.Rectangles.
unite the 2 rectangles, to get your cross, so you have one Path to operate on.
Use subtract instead of exclude to "punch through".
Here's an example:
var x = new paper.Path.Rectangle({
from: [100, 100],
to: [120, 200],
fillColor: 'red',
strokeWidth: 1
});
var y = x.clone().rotate(90).set({ fillColor: 'blue' })
// Unite x/y to get a single path.
var cross = y.unite(x)
// Remove x,y we no longer need them, we got the cross.
x.remove()
y.remove()
var hole = new paper.Path.Circle({
center:[110, 150],
radius: 6,
strokeColor: 'red',
fillColor: 'red'
})
// Subtract (instead of exclude), to "punch through".
var drilled = cross.subtract(hole)
// Remove hole/cross, we no longer need them.
hole.remove()
cross.remove()
console.log(drilled)
and here's a Sketch.
If you don't want to unite your shapes, you can still loop through them
and subtract the hole from them, just remember to use closed Paths.

How to select a single Curve in a Path in Paper.js?

I need to distinctly select a single Curve of a clicked Path, how can I do that?
For example, in this sketch we can select a whole path when clicked on it:
Currently I can detect the curve (not sure if it is the appropriate approach, anyway):
..onMouseDown = (event) ~>
hit = scope.project.hitTest event.point
if hit?item
# select only that specific segment
curves = hit.item.getCurves!
nearest = null
dist = null
for i, curve of curves
_dist = curve.getNearestPoint(event.point).getDistance(event.point)
if _dist < dist or not nearest?
nearest = i
dist = _dist
selected-curve = curves[nearest]
..selected = yes
But whole path is selected anyway:
What I want to achieve is something like this:
There is an easier way to achieve what you want.
You can know if hit was on a curve by checking its location property.
If it is set, you can easily get the curve points and manually draw your selection.
Here is a sketch demonstrating it.
var myline = new Path(new Point(100, 100));
myline.strokeColor = 'red';
myline.strokeWidth = 6;
myline.add(new Point(200, 100));
myline.add(new Point(260, 170));
myline.add(new Point(360, 170));
myline.add(new Point(420, 250));
function onMouseDown(event) {
hit = paper.project.hitTest(event.point);
// check if hit is on curve
if (hit && hit.location) {
// get curve
var curve = hit.location.curve;
// draw selection
var selection = new Group(
new Path.Line({
from: curve.point1,
to: curve.point2,
strokeColor: 'blue',
strokeWidth: 3
}),
new Path.Rectangle({
from: curve.point1 - 5,
to: curve.point1 + 5,
fillColor: 'blue'
}),
new Path.Rectangle({
from: curve.point2 - 5,
to: curve.point2 + 5,
fillColor: 'blue'
})
);
// make it automatically be removed on next down event
selection.removeOnDown();
}
}
Update
As an alternative, to avoid messing up with the exported drawing, you can simply select the line instead of applying it a stroke style.
See this sketch.
var selection = new Path.Line({
from: curve.point1,
to: curve.point2,
selected: true
});
There is no built-in way to do what you'd like AFAIK.
You basically need to walk through the segments, construct a line, and see if the hit is on that particular line. The line cannot be transparent or it's not considered a hit which is why I give it color and width to match the visible line; it's also why it's deleted after the test.
Here's the sketch solution that implements a bit more around this:
function onMouseDown(event){
if (!myline.hitTest(event.point)) {
return
}
c1.remove()
c2.remove()
// there's a hit so this should find it
let p = event.point
let segs = myline.segments
for (let i = 1; i < segs.length; i++) {
let line = new Path.Line(segs[i - 1].point, segs[i].point)
line.strokeWidth = 6
line.strokeColor = 'black'
if (line.hitTest(p)) {
c1 = new Path.Circle(segs[i-1].point, 6)
c2 = new Path.Circle(segs[i].point, 6)
c1.fillColor = 'black'
c2.fillColor = 'black'
line.remove()
return
}
line.remove()
}
throw new Error("could not find hit")
}
Here's what I draw:

Google Maps v3 Shapes

I am trying to take a string which has shape option information and create the shape on my Google Map application.
The string is made by splitting an array that was built from a local text document.
The string appears as:
Circle{center: new google.maps.LatLng(38.041872419557094, -87.6046371459961),radius:5197.017394363823,fillColor: '#000000',strokeWeight: 1,strokeColor: '#000000',map:map};
The function I have to take such string and make the shape appears as:
function loadDrawings(evt)
{
var f = evt.target.files[0];
if (!f)
{
alert("Failed to load file");
}
else if (!f.type.match('text.*'))
{
alert(f.name + " is not a valid text file.");
}
else
{
var r = new FileReader();
r.onload = function (e)
{
var contents = e.target.result;
var drawings = [];
var drawing;
var drawingType;
var shape;
var shapeOptions;
drawings = contents.split(";");
for (i = 0; i < drawings.length - 1; i++) {
drawing = drawings[i].toString();
drawingType = drawing.substr(0, drawing.indexOf('{'));
if (drawingType == "Circle")
{
shapeOptions = drawing.substr(6); //UNIQUE TO CIRCLE
shape = new google.maps.Circle(shapeOptions);
shape.setMap(map);
}
};
}
r.readAsText(f);
}
}
My issue is shapeOptions as a string does not work in the above syntax for creating the Circle. However, if I take the contents of the string, which is:
{center: new google.maps.LatLng(38.041872419557094, -87.6046371459961),radius:5197.017394363823,fillColor: '#000000',strokeWeight: 1,strokeColor: '#000000',map:map}
And directly enter it, the shape appears.
Do I need a certain variable type for my shapeOptions for this to work? I know that the new google.maps. requires (), but I have had no luck creating a variable from my string. Am I missing something here?
Much appreciation for any help!
Your shapeOptions string is a JavaScript object literal, so you can eval() it to get the object:
shapeOptions = eval( '(' + drawing.substr(6) + ')' );
Since it has map:map in it, you don't need the subsequent setMap() call.
Also, you're missing a var for the i variable. I don't really recommend the coding style where all the var statements go at the top of a function. I find it error-prone; it's too easy to omit a var without noticing it. (I know some famous JavaScript experts insist that var at the top is the only way to do it, but they fail to see the tradeoffs involved.)
You don't need the .toString() on drawings[i]. It's already a string.
You have two different brace styles. Best to pick one and stick with it. For JavaScript, putting the { on a line by itself is not recommended, because this code will not do what you expect:
return // hoping to return an object literal - but it doesn't!
{
a: 'b',
c: 'd'
}
Whereas this code does work correctly:
return {
a: 'b',
c: 'd'
}
Since you are using FileReader, I think it's safe to assume you also have .forEach() available.
You can replace the code that uses .indexOf() and the hard coded length with a regular expression.
Putting all that together, you might end up with code like this:
var r = new FileReader();
r.onload = function( e ) {
e.target.result.split(";").forEach( function( drawing ) {
var match = drawing.match( /^(\w+)({.*})$/ );
if( ! match ) return; // unrecognized
var type = match[0], options = eval( match[1] );
switch( type ) {
case "Circle":
new google.maps.Circle( options );
break;
}
});
}
r.readAsText( f );
But you may be able to take it a step further. So far we're looking at a Circle (line breaks added for readability):
Circle{
center: new google.maps.LatLng(
38.041872419557094,
-87.6046371459961
),
radius:5197.017394363823,
fillColor: '#000000',
strokeWeight: 1,
strokeColor: '#000000',
map:map
}
With only a simple change, that could be executed as JavaScript directly. You just need the 'new google.maps.' at the beginning and () around the object literal:
new google.maps.Circle({
center: new google.maps.LatLng(
38.041872419557094,
-87.6046371459961
),
radius:5197.017394363823,
fillColor: '#000000',
strokeWeight: 1,
strokeColor: '#000000',
map:map
})
I assume you will have other drawing types as well? Will they all map directly to google.maps.* objects like Circle does? If so, you could simply do:
var r = new FileReader();
r.onload = function( e ) {
e.target.result.split(";").forEach( function( drawing ) {
eval( drawing.replace(
/^(\w+)({.*})$/,
'new google.maps.$1(\$2)'
) );
});
}
r.readAsText( f );

Get latitude and longitude that is 5 metres away of a latitude and longitude [duplicate]

Bit stuck on this one. I am retrieving a list of geo coords via JSON and popping them onto a google map. All is working well except in the instance when I have two or more markers on the exact same spot. The API only displays 1 marker - the top one. This is fair enough I suppose but would like to find a way to display them all somehow.
I've searched google and found a few solutions but they mostly seem to be for V2 of the API or just not that great. Ideally I'd like a solution where you click some sort of group marker and that then shows the markers clustered around the spot they are all in.
Anybody had this problem or similar and would care to share a solution?
Take a look at OverlappingMarkerSpiderfier.
There's a demo page, but they don't show markers which are exactly on the same spot, only some which are very close together.
But a real life example with markers on the exact same spot can be seen on http://www.ejw.de/ejw-vor-ort/ (scroll down for the map and click on a few markers to see the spider-effect).
That seems to be the perfect solution for your problem.
Offsetting the markers isn't a real solution if they're located in the same building. What you might want to do is modify the markerclusterer.js like so:
Add a prototype click method in the MarkerClusterer class, like so - we will override this later in the map initialize() function:
MarkerClusterer.prototype.onClick = function() {
return true;
};
In the ClusterIcon class, add the following code AFTER the clusterclick trigger:
// Trigger the clusterclick event.
google.maps.event.trigger(markerClusterer, 'clusterclick', this.cluster_);
var zoom = this.map_.getZoom();
var maxZoom = markerClusterer.getMaxZoom();
// if we have reached the maxZoom and there is more than 1 marker in this cluster
// use our onClick method to popup a list of options
if (zoom >= maxZoom && this.cluster_.markers_.length > 1) {
return markerClusterer.onClickZoom(this);
}
Then, in your initialize() function where you initialize the map and declare your MarkerClusterer object:
markerCluster = new MarkerClusterer(map, markers);
// onClickZoom OVERRIDE
markerCluster.onClickZoom = function() { return multiChoice(markerCluster); }
Where multiChoice() is YOUR (yet to be written) function to popup an InfoWindow with a list of options to select from. Note that the markerClusterer object is passed to your function, because you will need this to determine how many markers there are in that cluster. For example:
function multiChoice(mc) {
var cluster = mc.clusters_;
// if more than 1 point shares the same lat/long
// the size of the cluster array will be 1 AND
// the number of markers in the cluster will be > 1
// REMEMBER: maxZoom was already reached and we can't zoom in anymore
if (cluster.length == 1 && cluster[0].markers_.length > 1)
{
var markers = cluster[0].markers_;
for (var i=0; i < markers.length; i++)
{
// you'll probably want to generate your list of options here...
}
return false;
}
return true;
}
I used this alongside jQuery and it does the job:
var map;
var markers = [];
var infoWindow;
function initialize() {
var center = new google.maps.LatLng(-29.6833300, 152.9333300);
var mapOptions = {
zoom: 5,
center: center,
panControl: false,
zoomControl: false,
mapTypeControl: false,
scaleControl: false,
streetViewControl: false,
overviewMapControl: false,
mapTypeId: google.maps.MapTypeId.ROADMAP
}
map = new google.maps.Map(document.getElementById('map-canvas'), mapOptions);
$.getJSON('jsonbackend.php', function(data) {
infoWindow = new google.maps.InfoWindow();
$.each(data, function(key, val) {
if(val['LATITUDE']!='' && val['LONGITUDE']!='')
{
// Set the coordonates of the new point
var latLng = new google.maps.LatLng(val['LATITUDE'],val['LONGITUDE']);
//Check Markers array for duplicate position and offset a little
if(markers.length != 0) {
for (i=0; i < markers.length; i++) {
var existingMarker = markers[i];
var pos = existingMarker.getPosition();
if (latLng.equals(pos)) {
var a = 360.0 / markers.length;
var newLat = pos.lat() + -.00004 * Math.cos((+a*i) / 180 * Math.PI); //x
var newLng = pos.lng() + -.00004 * Math.sin((+a*i) / 180 * Math.PI); //Y
var latLng = new google.maps.LatLng(newLat,newLng);
}
}
}
// Initialize the new marker
var marker = new google.maps.Marker({map: map, position: latLng, title: val['TITLE']});
// The HTML that is shown in the window of each item (when the icon it's clicked)
var html = "<div id='iwcontent'><h3>"+val['TITLE']+"</h3>"+
"<strong>Address: </strong>"+val['ADDRESS']+", "+val['SUBURB']+", "+val['STATE']+", "+val['POSTCODE']+"<br>"+
"</div>";
// Binds the infoWindow to the point
bindInfoWindow(marker, map, infoWindow, html);
// Add the marker to the array
markers.push(marker);
}
});
// Make a cluster with the markers from the array
var markerCluster = new MarkerClusterer(map, markers, { zoomOnClick: true, maxZoom: 15, gridSize: 20 });
});
}
function markerOpen(markerid) {
map.setZoom(22);
map.panTo(markers[markerid].getPosition());
google.maps.event.trigger(markers[markerid],'click');
switchView('map');
}
google.maps.event.addDomListener(window, 'load', initialize);
Expanding on Chaoley's answer, I implemented a function that, given a list of locations (objects with lng and lat properties) whose coordinates are exactly the same, moves them away from their original location a little bit (modifying objects in place). They then form a nice circle around the center point.
I found that, for my latitude (52deg North), 0.0003 degrees of circle radius work best, and that you have to make up for the difference between latitude and longitude degrees when converted to kilometres. You can find approximate conversions for your latitude here.
var correctLocList = function (loclist) {
var lng_radius = 0.0003, // degrees of longitude separation
lat_to_lng = 111.23 / 71.7, // lat to long proportion in Warsaw
angle = 0.5, // starting angle, in radians
loclen = loclist.length,
step = 2 * Math.PI / loclen,
i,
loc,
lat_radius = lng_radius / lat_to_lng;
for (i = 0; i < loclen; ++i) {
loc = loclist[i];
loc.lng = loc.lng + (Math.cos(angle) * lng_radius);
loc.lat = loc.lat + (Math.sin(angle) * lat_radius);
angle += step;
}
};
#Ignatius most excellent answer, updated to work with v2.0.7 of MarkerClustererPlus.
Add a prototype click method in the MarkerClusterer class, like so - we will override this later in the map initialize() function:
// BEGIN MODIFICATION (around line 715)
MarkerClusterer.prototype.onClick = function() {
return true;
};
// END MODIFICATION
In the ClusterIcon class, add the following code AFTER the click/clusterclick trigger:
// EXISTING CODE (around line 143)
google.maps.event.trigger(mc, "click", cClusterIcon.cluster_);
google.maps.event.trigger(mc, "clusterclick", cClusterIcon.cluster_); // deprecated name
// BEGIN MODIFICATION
var zoom = mc.getMap().getZoom();
// Trying to pull this dynamically made the more zoomed in clusters not render
// when then kind of made this useless. -NNC # BNB
// var maxZoom = mc.getMaxZoom();
var maxZoom = 15;
// if we have reached the maxZoom and there is more than 1 marker in this cluster
// use our onClick method to popup a list of options
if (zoom >= maxZoom && cClusterIcon.cluster_.markers_.length > 1) {
return mc.onClick(cClusterIcon);
}
// END MODIFICATION
Then, in your initialize() function where you initialize the map and declare your MarkerClusterer object:
markerCluster = new MarkerClusterer(map, markers);
// onClick OVERRIDE
markerCluster.onClick = function(clickedClusterIcon) {
return multiChoice(clickedClusterIcon.cluster_);
}
Where multiChoice() is YOUR (yet to be written) function to popup an InfoWindow with a list of options to select from. Note that the markerClusterer object is passed to your function, because you will need this to determine how many markers there are in that cluster. For example:
function multiChoice(clickedCluster) {
if (clickedCluster.getMarkers().length > 1)
{
// var markers = clickedCluster.getMarkers();
// do something creative!
return false;
}
return true;
};
This is more of a stopgap 'quick and dirty' solution similar to the one Matthew Fox suggests, this time using JavaScript.
In JavaScript you can just offset the lat and long of all of your locations by adding a small random offset to both e.g.
myLocation[i].Latitude+ = (Math.random() / 25000)
(I found that dividing by 25000 gives enough separation but doesn't move the marker significantly from the exact location e.g. a specific address)
This makes a reasonably good job of offsetting them from one another, but only after you've zoomed in closely. When zoomed out, it still won't be clear that there are multiple options for the location.
The answers above are more elegant, but I found a quick and dirty way that actually works really really incredibly well. You can see it in action at www.buildinglit.com
All I did was add a random offset to the latitude and longditude to my genxml.php page so it returns slightly different results each time with offset each time the map is created with markers. This sounds like a hack, but in reality you only need the markers to move a slight nudge in a random direction for them to be clickable on the map if they are overlapping. It actually works really well, I would say better than the spider method because who wants to deal with that complexity and have them spring everywhere. You just want to be able to select the marker. Nudging it randomly works perfect.
Here is an example of the while statement iteration node creation in my php_genxml.php
while ($row = #mysql_fetch_assoc($result)){ $offset = rand(0,1000)/10000000;
$offset2 = rand(0, 1000)/10000000;
$node = $dom->createElement("marker");
$newnode = $parnode->appendChild($node);
$newnode->setAttribute("name", $row['name']);
$newnode->setAttribute("address", $row['address']);
$newnode->setAttribute("lat", $row['lat'] + $offset);
$newnode->setAttribute("lng", $row['lng'] + $offset2);
$newnode->setAttribute("distance", $row['distance']);
$newnode->setAttribute("type", $row['type']);
$newnode->setAttribute("date", $row['date']);
$newnode->setAttribute("service", $row['service']);
$newnode->setAttribute("cost", $row['cost']);
$newnode->setAttribute("company", $company);
Notice under lat and long there is the +offset. from the 2 variables above. I had to divide random by 0,1000 by 10000000 in order to get a decimal that was randomly small enough to just barely move the markers around. Feel free to tinker with that variable to get one that is more precise for your needs.
I like simple solutions so here's mine.
Instead of modifying the lib, which would make it harder to mantain. you can simply watch the event like this
google.maps.event.addListener(mc, "clusterclick", onClusterClick);
then you can manage it on
function onClusterClick(cluster){
var ms = cluster.getMarkers();
i, ie, used bootstrap to show a panel with a list. which i find much more confortable and usable than spiderfying on "crowded" places. (if you are using a clusterer chances are you will end up with collisions once you spiderfy).
you can check the zoom there too.
btw. i just found leaflet and it seems to work much better, the cluster AND spiderfy works very fluidly http://leaflet.github.io/Leaflet.markercluster/example/marker-clustering-realworld.10000.html
and it's open-source.
For situations where there are multiple services in the same building you could offset the markers just a little, (say by .001 degree), in a radius from the actual point. This should also produce a nice visual effect.
Check out Marker Clusterer for V3 - this library clusters nearby points into a group marker. The map zooms in when the clusters are clicked. I'd imagine when zoomed right in you'd still have the same problem with markers on the same spot though.
Updated to work with MarkerClustererPlus.
google.maps.event.trigger(mc, "click", cClusterIcon.cluster_);
google.maps.event.trigger(mc, "clusterclick", cClusterIcon.cluster_); // deprecated name
// BEGIN MODIFICATION
var zoom = mc.getMap().getZoom();
// Trying to pull this dynamically made the more zoomed in clusters not render
// when then kind of made this useless. -NNC # BNB
// var maxZoom = mc.getMaxZoom();
var maxZoom = 15;
// if we have reached the maxZoom and there is more than 1 marker in this cluster
// use our onClick method to popup a list of options
if (zoom >= maxZoom && cClusterIcon.cluster_.markers_.length > 1) {
var markers = cClusterIcon.cluster_.markers_;
var a = 360.0 / markers.length;
for (var i=0; i < markers.length; i++)
{
var pos = markers[i].getPosition();
var newLat = pos.lat() + -.00004 * Math.cos((+a*i) / 180 * Math.PI); // x
var newLng = pos.lng() + -.00004 * Math.sin((+a*i) / 180 * Math.PI); // Y
var finalLatLng = new google.maps.LatLng(newLat,newLng);
markers[i].setPosition(finalLatLng);
markers[i].setMap(cClusterIcon.cluster_.map_);
}
cClusterIcon.hide();
return ;
}
// END MODIFICATION
I used markerclustererplus, and for me this works:
//Code
google.maps.event.addListener(cMarkerClusterer, "clusterclick", function (c) {
var markers = c.getMarkers();
//Check Markers array for duplicate position and offset a little
if (markers .length > 1) {
//Check if all markers are in the same position (with 4 significant digits)
if (markers .every((val, index, arr) => (val.getPosition().lat().toFixed(4) == arr[0].getPosition().lat().toFixed(4)) && (val.getPosition().lng().toFixed(4) == arr[0].getPosition().lng().toFixed(4)))) { /
//Don't modify first element
for (i = 1; i < markers.length; i++) {
var existingMarker = markers[i];
var pos = existingMarker.getPosition();
var quot = 360.0 / markers.length;
var newLat = pos.lat() + -.00008 * Math.cos(+quot * i); //+ -.00008 * Math.cos((+quot * i) / 180 * Math.PI); //x
var newLng = pos.lng() + -.00008 * Math.sin(+quot * i); //+ -.0008 * Math.sin((+quot * i) / 180 * Math.PI); //Y
existingMarker.setPosition(new google.maps.LatLng(newLat, newLng));
}
let cZoom = map.getZoom();
map.setZoom(cZoom-1);
map.setZoom(cZoom+1);
}
}
});
Check this: https://github.com/plank/MarkerClusterer
This is the MarkerCluster modified to have a infoWindow in a cluster marker, when you have several markers in the same position.
You can see how it works here: http://culturedays.ca/en/2013-activities
Giving offset will make the markers faraway when the user zoom in to max. So i found a way to achieve that. this may not be a proper way but it worked very well.
// This code is in swift
for loop markers
{
//create marker
let mapMarker = GMSMarker()
mapMarker.groundAnchor = CGPosition(0.5, 0.5)
mapMarker.position = //set the CLLocation
//instead of setting marker.icon set the iconView
let image:UIIMage = UIIMage:init(named:"filename")
let imageView:UIImageView = UIImageView.init(frame:rect(0,0, ((image.width/2 * markerIndex) + image.width), image.height))
imageView.contentMode = .Right
imageView.image = image
mapMarker.iconView = imageView
mapMarker.map = mapView
}
set the zIndex of the marker so that you will see the marker icon which you want to see on top, otherwise it will animate the markers like auto swapping. when the user tap the marker handle the zIndex to bring the marker on top using zIndex Swap.
How to get away with it..
[Swift]
var clusterArray = [String]()
var pinOffSet : Double = 0
var pinLat = yourLat
var pinLong = yourLong
var location = pinLat + pinLong
A new marker is about to be created? check clusterArray and manipulate it's offset
if(!clusterArray.contains(location)){
clusterArray.append(location)
} else {
pinOffSet += 1
let offWithIt = 0.00025 // reasonable offset with zoomLvl(14-16)
switch pinOffSet {
case 1 : pinLong = pinLong + offWithIt ; pinLat = pinLat + offWithIt
case 2 : pinLong = pinLong + offWithIt ; pinLat = pinLat - offWithIt
case 3 : pinLong = pinLong - offWithIt ; pinLat = pinLat - offWithIt
case 4 : pinLong = pinLong - offWithIt ; pinLat = pinLat + offWithIt
default : print(1)
}
}
result
Adding to Matthew Fox's sneaky genius answer, I have added a small random offset to each lat and lng when setting the marker object. For example:
new LatLng(getLat()+getMarkerOffset(), getLng()+getMarkerOffset()),
private static double getMarkerOffset(){
//add tiny random offset to keep markers from dropping on top of themselves
double offset =Math.random()/4000;
boolean isEven = ((int)(offset *400000)) %2 ==0;
if (isEven) return offset;
else return -offset;
}
I used this http://leaflet.github.io/Leaflet.markercluster/ and perfectly works for me. added full solution.
<html lang="en">
<head>
<script src="https://code.jquery.com/jquery-3.2.1.js" integrity="sha256-DZAnKJ/6XZ9si04Hgrsxu/8s717jcIzLy3oi35EouyE=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.0.3/leaflet.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.0.4/leaflet.markercluster.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.0.3/leaflet.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.0.4/MarkerCluster.Default.css" />
</head>
<body>
<div id="map"></div>
<script>
var addressData = [
{id: 9, name: "Ankita", title: "Manager", latitude: "33.1128019", longitude: "-96.6958939"},
{id: 1, name: "Aarti", title: "CEO", latitude: "33.1128019", longitude: "-96.6958939"},
{id: 2, name: "Payal", title: "Employee", latitude: "33.0460488", longitude: "-96.9983386"}];
var addressPoints = [];
for (i = 0; i < addressData.length; i++) {
var marker = {
latitude: addressData[i].latitude,
longitude: addressData[i].longitude,
coverage: addressData[i]
};
addressPoints.push(marker);
}
var map = L.map('map').setView(["32.9602172", "-96.7036844"], 5);
var basemap = L.tileLayer('http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png', {attribution: '© OpenStreetMap © CartoDB', subdomains: 'abcd'});
basemap.addTo(map);
var markers = L.markerClusterGroup();
for (var i = 0; i < addressPoints.length; i++) {
// var icon1 = "app/common_assest/images/pin/redPin.png"; // set ehere you own marker pin whatever you want to set
var currentMarker = addressPoints[i];
console.log(currentMarker);
var contentString = '<div class="mapinfoWindowContent">' +
'<div class="mapInfoTitle">Name: ' + currentMarker.coverage.name + '</div>' +
'<div class="mapInfoSubText">Licence: ' + currentMarker.coverage.title + '</div>' +
'</div>';
// var myIcon = L.icon({// set ehere you own marker pin whatever you want to set
// iconUrl: icon1,
// iconRetinaUrl: icon1,
// });
var marker = L.marker(new L.LatLng(currentMarker['latitude'], currentMarker['longitude']), {
title: currentMarker.coverage.name
});
marker.bindPopup(contentString);
markers.addLayer(marker);
}
markers.addTo(map);
</script>
</body>
Hope fully it will help to you easily.
The solution I've used is pretty simple. Just use #googlemaps/markerclusterer library in combination with the Maps JavaScript API.
Than you will need just one line after the map is filled out with your markers:
new MarkerClusterer({ map, markers });
All information can be found here
https://developers.google.com/maps/documentation/javascript/marker-clustering
I'm using Android's Map Cluster. These are the libs I'm using:
implementation 'com.google.android.gms:play-services-places:16.0.0' 
implementation 'com.google.android.gms:play-services-maps:16.0.0'
implementation 'com.google.android.gms:play-services-location:16.0.0'
implementation 'com.google.maps.android:android-maps-utils:2.0.1'
The problem I was running into is that the Cluster Markers don't separate if two items have the exact same Latitude and Longitudinal points. My fix is to scan through my array of items and if two positions match, I move their positions slightly. Here's my code:
Field Variables:
private ArrayList<Tool> esTools;
When you're done initializing the ArrayList of Tools. From your parsing method, call this:
loopThroughToolsListAndFixOnesThatHaveSameGeoPoint_FixStackingIssue();
Where the magic happens:
private void loopThroughToolsListAndFixOnesThatHaveSameGeoPoint_FixStackingIssue() {
DecimalFormat decimalFormatTool = new DecimalFormat("000.0000");
decimalFormatTool.setRoundingMode(RoundingMode.DOWN);
for(int backPointer=0; backPointer <= (esTools.size()-1); backPointer++){
Map<String, Double> compareA = esTools.get(backPointer).getUserChosenGeopoint();
Double compareA_Latitude = compareA.get("_latitude");
compareA_Latitude= Double.valueOf(decimalFormatTool.format(compareA_Latitude));
Double compareA_Longitude = compareA.get("_longitude");
compareA_Longitude= Double.valueOf(decimalFormatTool.format(compareA_Longitude));
System.out.println("compareA_Lat= "+ compareA_Latitude+ ", compareA_Long= "+ compareA_Longitude);
for(int frontPointer=0; frontPointer <= (esTools.size()-1); frontPointer++){
if(backPointer==frontPointer){
continue;
}
Map<String, Double> compareB = esTools.get(frontPointer).getUserChosenGeopoint();
Double compareB_Latitude = compareB.get("_latitude");
compareB_Latitude= Double.valueOf(decimalFormatTool.format(compareB_Latitude));
Double compareB_Longitude = compareB.get("_longitude");
compareB_Longitude= Double.valueOf(decimalFormatTool.format(compareB_Longitude));
if((compareB_Latitude.equals(compareA_Latitude)) && (compareB_Longitude.equals(compareA_Longitude))) {
System.out.println("these tools match");
Random randomGen = new Random();
Double randomNumLat = randomGen.nextDouble() * 0.00015;
int addOrSubtractLatitude= ( randomGen.nextBoolean() ? 1 : -1 );
randomNumLat = randomNumLat*addOrSubtractLatitude;
Double randomNumLong = randomGen.nextDouble() * 0.00015;
int addOrSubtractLongitude= ( randomGen.nextBoolean() ? 1 : -1 );
randomNumLong = randomNumLong*addOrSubtractLongitude;
System.out.println("Adding Random Latitude="+ randomNumLat + ", Longitude= "+ randomNumLong);
System.out.println("\n");
Map<String, Double> latitudeLongitude = new HashMap<>();
latitudeLongitude.put("_latitude", (compareB_Latitude+ randomNumLat));
latitudeLongitude.put("_longitude", (compareB_Longitude+ randomNumLong));
esTools.get(frontPointer).setUserChosenGeopoint(latitudeLongitude);
}
}
}
}
So what the above method does is scan through my ArrayList and see if there are any two Tools have matching points. If the Lat Long points match, move one slightly.
Expanding on the answers given above, just ensure you set maxZoom option when initializing the map object.
Adding to above answers but offering an alternative quick solution in php and wordpress. For this example I am storing the location field via ACF and looping through the posts to grab that data.
I found that storing the lat / lng in an array and check the value of that array to see if the loop matches, we can then update the value within that array with the amount we want to shift our pips by.
//This is the value to shift the pips by. I found this worked best with markerClusterer
$add_to_latlng = 0.00003;
while ($query->have_posts()) {
$query->the_post();
$meta = get_post_meta(get_the_ID(), "postcode", true); //uses an acf field to store location
$lat = $meta["lat"];
$lng = $meta["lng"];
if(in_array($meta["lat"],$lat_checker)){ //check if this meta matches
//if matches then update the array to a new value (current value + shift value)
// This is using the lng value for a horizontal line of pips, use lat for vertical, or both for a diagonal
if(isset($latlng_storer[$meta["lng"]])){
$latlng_storer[$meta["lng"]] = $latlng_storer[$meta["lng"]] + $add_to_latlng;
$lng = $latlng_storer[$meta["lng"]];
} else {
$latlng_storer[$meta["lng"]] = $meta["lng"];
$lng = $latlng_storer[$meta["lng"]];
}
} else {
$lat_checker[] = $meta["lat"]; //just for simple checking of data
$latlng_storer[$meta["lat"]] = floatval($meta["lat"]);
$latlng_storer[$meta["lng"]] = floatval($meta["lng"]);
}
$entry[] = [
"lat" => $lat,
"lng" => $lng,
//...Add all the other post data here and use this array for the pips
];
} // end query
Once I've grabbed these locations I json encode the $entry variable and use that within my JS.
let locations = <?=json_encode($entry)?>;
I know this is a rather specific situation but I hope this helps someone along the line!
Extending answers above, when you got joined strings, not added/subtracted position (e.g. "37.12340-0.00069"), convert your original lat/longitude to floats, e.g. using parseFloat(), then add or subtract corrections.

Resources