[shotwell] map: Group Markers based on a raster on the map.



commit a1163ffa9aaf5c16a32a443e5939ab7c5be06ba2
Author: Andreas Brauchli <a brauchli elementarea net>
Date:   Wed Jun 15 21:28:37 2016 +0200

    map: Group Markers based on a raster on the map.
    
    Create one group marker where multiple markers are in close vicinity.
    The raster is fixed (width and height at 30 px in this commit)
    and the markers are all regrouped each time the zoom level is changed.
    
    The group marker's position is set to the average of all contained markers.

 .../scalable/actions/gps-markers-many-selected.svg | 477 ++++++++++++++++++++
 .../hicolor/scalable/actions/gps-markers-many.svg  | 477 ++++++++++++++++++++
 src/MapWidget.vala                                 | 493 ++++++++++++++-------
 src/Properties.vala                                |   4 +-
 src/Resources.vala                                 |   2 +
 5 files changed, 1281 insertions(+), 172 deletions(-)
---
diff --git a/data/icons/hicolor/scalable/actions/gps-markers-many-selected.svg 
b/data/icons/hicolor/scalable/actions/gps-markers-many-selected.svg
new file mode 100644
index 00000000..0757f26a
--- /dev/null
+++ b/data/icons/hicolor/scalable/actions/gps-markers-many-selected.svg
@@ -0,0 +1,477 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/";
+   xmlns:cc="http://creativecommons.org/ns#";
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+   xmlns:svg="http://www.w3.org/2000/svg";
+   xmlns="http://www.w3.org/2000/svg";
+   xmlns:xlink="http://www.w3.org/1999/xlink";
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd";
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape";
+   width="28.74"
+   height="38.98"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.91 r13725"
+   sodipodi:docname="gps-markers-many-selected.svg">
+  <defs
+     id="defs4">
+    <linearGradient
+       id="linearGradient3887">
+      <stop
+         id="stop3889"
+         offset="0"
+         style="stop-color:#ff573f;stop-opacity:1;" />
+      <stop
+         id="stop3891"
+         offset="1"
+         style="stop-color:#b71111;stop-opacity:1;" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       id="linearGradient3857">
+      <stop
+         style="stop-color:#87b5f5;stop-opacity:1;"
+         offset="0"
+         id="stop3859" />
+      <stop
+         style="stop-color:#87b5f5;stop-opacity:0;"
+         offset="1"
+         id="stop3861" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       id="linearGradient3849">
+      <stop
+         style="stop-color:#ffffff;stop-opacity:1;"
+         offset="0"
+         id="stop3851" />
+      <stop
+         style="stop-color:#ffffff;stop-opacity:0;"
+         offset="1"
+         id="stop3853" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3827">
+      <stop
+         id="stop3829"
+         offset="0"
+         style="stop-color:#50a9ff;stop-opacity:1;" />
+      <stop
+         id="stop3831"
+         offset="1"
+         style="stop-color:#0034a9;stop-opacity:0.92490119;" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3821">
+      <stop
+         id="stop3823"
+         offset="0"
+         style="stop-color:#60aaf1;stop-opacity:1;" />
+      <stop
+         id="stop3825"
+         offset="1"
+         style="stop-color:#124cd1;stop-opacity:0.92490119;" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3787">
+      <stop
+         style="stop-color:#535353;stop-opacity:1;"
+         offset="0"
+         id="stop3789" />
+      <stop
+         style="stop-color:#ffffff;stop-opacity:0;"
+         offset="1"
+         id="stop3791" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3765">
+      <stop
+         style="stop-color:#23b3ff;stop-opacity:1;"
+         offset="0"
+         id="stop3767" />
+      <stop
+         style="stop-color:#124cd1;stop-opacity:0.92490119;"
+         offset="1"
+         id="stop3769" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3765"
+       id="linearGradient3771"
+       x1="381.42856"
+       y1="335.09586"
+       x2="381.42856"
+       y2="567.15851"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.13306152,0,0,0.13306152,324.43662,388.73998)" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3787"
+       id="radialGradient3793"
+       cx="374.25"
+       cy="464.11218"
+       fx="374.25"
+       fy="464.11218"
+       r="8.25"
+       gradientTransform="matrix(1,0,0,0.33333333,0,309.40812)"
+       gradientUnits="userSpaceOnUse" />
+    <filter
+       inkscape:collect="always"
+       id="filter3803"
+       x="-0.096096098"
+       width="1.1921922"
+       y="-0.2882883"
+       height="1.5765766"
+       color-interpolation-filters="sRGB">
+      <feGaussianBlur
+         inkscape:collect="always"
+         stdDeviation="0.66066066"
+         id="feGaussianBlur3805" />
+    </filter>
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3787"
+       id="radialGradient3843"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1,0,0,0.33333333,0,309.40812)"
+       cx="374.25"
+       cy="464.11218"
+       fx="374.25"
+       fy="464.11218"
+       r="8.25" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3765"
+       id="linearGradient3845"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.13306152,0,0,0.13306152,394.43662,388.73998)"
+       x1="381.42856"
+       y1="335.09586"
+       x2="381.42856"
+       y2="567.15851" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3849"
+       id="linearGradient3855"
+       x1="461.5"
+       y1="477.36218"
+       x2="462.5"
+       y2="434.36218"
+       gradientUnits="userSpaceOnUse" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3857"
+       id="linearGradient3863"
+       x1="444.95898"
+       y1="433.89029"
+       x2="444.95898"
+       y2="454.77341"
+       gradientUnits="userSpaceOnUse" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3849"
+       id="linearGradient3871"
+       x1="382.17749"
+       y1="377.47879"
+       x2="382.17749"
+       y2="414.47479"
+       gradientUnits="userSpaceOnUse" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3787"
+       id="radialGradient3883"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1,0,0,0.33333333,0,309.40812)"
+       cx="374.25"
+       cy="464.11218"
+       fx="374.25"
+       fy="464.11218"
+       r="8.25" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3887"
+       id="linearGradient3885"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.13306152,0,0,0.13306152,270.34295,388.73998)"
+       x1="381.42856"
+       y1="335.09586"
+       x2="381.42856"
+       y2="567.15851" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3787"
+       id="radialGradient3925"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1,0,0,0.33333333,0,309.40812)"
+       cx="374.25"
+       cy="464.11218"
+       fx="374.25"
+       fy="464.11218"
+       r="8.25" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3765"
+       id="linearGradient3927"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.13306152,0,0,0.13306152,324.43662,388.73998)"
+       x1="381.42856"
+       y1="335.09586"
+       x2="381.42856"
+       y2="567.15851" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3787"
+       id="radialGradient3941"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1,0,0,0.33333333,0,309.40812)"
+       cx="374.25"
+       cy="464.11218"
+       fx="374.25"
+       fy="464.11218"
+       r="8.25" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3765"
+       id="linearGradient3943"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.13306152,0,0,0.13306152,324.43662,388.73998)"
+       x1="381.42856"
+       y1="335.09586"
+       x2="381.42856"
+       y2="567.15851" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3787"
+       id="radialGradient3945"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1,0,0,0.33333333,0,309.40812)"
+       cx="374.25"
+       cy="464.11218"
+       fx="374.25"
+       fy="464.11218"
+       r="8.25" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3765"
+       id="linearGradient3947"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.13306152,0,0,0.13306152,270.34295,388.73998)"
+       x1="381.42856"
+       y1="335.09586"
+       x2="381.42856"
+       y2="567.15851" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3787"
+       id="radialGradient3506"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1,0,0,0.33333333,0,309.40812)"
+       cx="374.25"
+       cy="464.11218"
+       fx="374.25"
+       fy="464.11218"
+       r="8.25" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3765"
+       id="linearGradient3508"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.13306152,0,0,0.13306152,270.34295,388.73998)"
+       x1="381.42856"
+       y1="335.09586"
+       x2="381.42856"
+       y2="567.15851" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3787"
+       id="radialGradient3522"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1,0,0,0.33333333,0,309.40812)"
+       cx="374.25"
+       cy="464.11218"
+       fx="374.25"
+       fy="464.11218"
+       r="8.25" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3765"
+       id="linearGradient3524"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.13306152,0,0,0.13306152,270.34295,388.73998)"
+       x1="381.42856"
+       y1="335.09586"
+       x2="381.42856"
+       y2="567.15851" />
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="11.313708"
+     inkscape:cx="14.057469"
+     inkscape:cy="24.266563"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:snap-global="false"
+     fit-margin-left="1"
+     units="px"
+     fit-margin-top="1"
+     fit-margin-right="1"
+     fit-margin-bottom="1"
+     inkscape:window-width="1440"
+     inkscape:window-height="836"
+     inkscape:window-x="0"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1">
+    <inkscape:grid
+       type="xygrid"
+       id="grid2987"
+       empspacing="5"
+       visible="true"
+       enabled="true"
+       snapvisiblegridlinesonly="true"
+       originx="-306.53522px"
+       originy="-506.68905px" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage"; />
+        <dc:title></dc:title>
+        <cc:license
+           rdf:resource="" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Ebene 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-306.53522,-506.69183)">
+    <g
+       id="g3035"
+       transform="translate(0,75.224057)">
+      <ellipse
+         ry="2.75"
+         rx="8.25"
+         cy="464.11218"
+         cx="374.25"
+         
style="opacity:0.58662612;fill:url(#radialGradient3945);fill-opacity:1;stroke:#none;filter:url(#filter3803)"
+         id="path3873"
+         transform="matrix(1.3594635,0,0,1,-187.87288,1)" />
+      <path
+         
style="fill:url(#linearGradient3947);fill-opacity:1.0;stroke:#0c438d;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         d="m 320.90633,432.9663 c -5.14414,0 -9.3143,4.17016 -9.3143,9.31432 0,1.76829 0.91939,4.12348 
1.34724,4.82763 0.42786,0.70414 7.79657,17.16494 7.79657,17.16494 0,0 7.58026,-16.29039 8.03775,-17.01109 
0.45749,-0.7207 1.44704,-3.14782 1.44704,-4.98148 0,-5.14416 -4.17016,-9.31432 -9.3143,-9.31432 z"
+         id="path3875"
+         inkscape:connector-curvature="0"
+         sodipodi:nodetypes="sszczss" />
+      <path
+         sodipodi:nodetypes="sszczss"
+         inkscape:connector-curvature="0"
+         id="path3877"
+         d="m 320.90633,434.00353 c -4.50028,0 -8.14847,3.69949 -8.14847,8.26307 0,1.56873 0.80432,3.6581 
1.17861,4.28277 0.3743,0.62468 6.82071,15.22767 6.82071,15.22767 0,0 6.63148,-14.45182 7.03171,-15.09118 
0.40022,-0.63936 1.26591,-2.79256 1.26591,-4.41926 0,-4.56358 -3.64819,-8.26307 -8.14847,-8.26307 z"
+         
style="fill:none;stroke:#5e9cf1;stroke-width:1.03512061;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
 />
+      <circle
+         r="18.75"
+         cy="396.11218"
+         cx="381.25"
+         transform="matrix(0.23144871,0,0,0.23144871,232.66651,350.69905)"
+         id="path3879"
+         
style="fill:#ffffff;fill-opacity:1;stroke:#5e9cf1;stroke-width:4.32061148;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.69999999;stroke-opacity:1"
 />
+      <circle
+         r="18.75"
+         cy="396.11218"
+         cx="381.25"
+         
style="fill:#ffffff;fill-opacity:1;stroke:#0c438d;stroke-width:6.2157526;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.69999999;stroke-opacity:1"
+         id="path3881"
+         transform="matrix(0.17705667,0,0,0.17705667,253.40348,372.2444)" />
+    </g>
+    <g
+       transform="matrix(0.62943981,0,0,0.62943981,124.11992,251.22885)"
+       id="g3494">
+      <ellipse
+         transform="matrix(1.3594635,0,0,1,-187.87288,1)"
+         id="ellipse3496"
+         
style="opacity:0.58662612;fill:url(#radialGradient3506);fill-opacity:1;stroke:#none;filter:url(#filter3803)"
+         cx="374.25"
+         cy="464.11218"
+         rx="8.25"
+         ry="2.75" />
+      <path
+         sodipodi:nodetypes="sszczss"
+         inkscape:connector-curvature="0"
+         id="path3498"
+         d="m 320.90633,432.9663 c -5.14414,0 -9.3143,4.17016 -9.3143,9.31432 0,1.76829 0.91939,4.12348 
1.34724,4.82763 0.42786,0.70414 7.79657,17.16494 7.79657,17.16494 0,0 7.58026,-16.29039 8.03775,-17.01109 
0.45749,-0.7207 1.44704,-3.14782 1.44704,-4.98148 0,-5.14416 -4.17016,-9.31432 -9.3143,-9.31432 z"
+         
style="fill:url(#linearGradient3508);fill-opacity:1.0;stroke:#0c438d;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
 />
+      <path
+         
style="fill:none;stroke:#5e9cf1;stroke-width:1.03512061;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         d="m 320.90633,434.00353 c -4.50028,0 -8.14847,3.69949 -8.14847,8.26307 0,1.56873 0.80432,3.6581 
1.17861,4.28277 0.3743,0.62468 6.82071,15.22767 6.82071,15.22767 0,0 6.63148,-14.45182 7.03171,-15.09118 
0.40022,-0.63936 1.26591,-2.79256 1.26591,-4.41926 0,-4.56358 -3.64819,-8.26307 -8.14847,-8.26307 z"
+         id="path3500"
+         inkscape:connector-curvature="0"
+         sodipodi:nodetypes="sszczss" />
+      <circle
+         
style="fill:#ffffff;fill-opacity:1;stroke:#5e9cf1;stroke-width:4.32061148;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.69999999;stroke-opacity:1"
+         id="circle3502"
+         transform="matrix(0.23144871,0,0,0.23144871,232.66651,350.69905)"
+         cx="381.25"
+         cy="396.11218"
+         r="18.75" />
+      <circle
+         transform="matrix(0.17705667,0,0,0.17705667,253.40348,372.2444)"
+         id="circle3504"
+         
style="fill:#ffffff;fill-opacity:1;stroke:#0c438d;stroke-width:6.2157526;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.69999999;stroke-opacity:1"
+         cx="381.25"
+         cy="396.11218"
+         r="18.75" />
+    </g>
+    <g
+       id="g3510"
+       transform="matrix(0.62943981,0,0,0.62943981,113.99492,248.22885)">
+      <ellipse
+         ry="2.75"
+         rx="8.25"
+         cy="464.11218"
+         cx="374.25"
+         
style="opacity:0.58662612;fill:url(#radialGradient3522);fill-opacity:1;stroke:#none;filter:url(#filter3803)"
+         id="ellipse3512"
+         transform="matrix(1.3594635,0,0,1,-187.87288,1)" />
+      <path
+         
style="fill:url(#linearGradient3524);fill-opacity:1.0;stroke:#0c438d;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         d="m 320.90633,432.9663 c -5.14414,0 -9.3143,4.17016 -9.3143,9.31432 0,1.76829 0.91939,4.12348 
1.34724,4.82763 0.42786,0.70414 7.79657,17.16494 7.79657,17.16494 0,0 7.58026,-16.29039 8.03775,-17.01109 
0.45749,-0.7207 1.44704,-3.14782 1.44704,-4.98148 0,-5.14416 -4.17016,-9.31432 -9.3143,-9.31432 z"
+         id="path3514"
+         inkscape:connector-curvature="0"
+         sodipodi:nodetypes="sszczss" />
+      <path
+         sodipodi:nodetypes="sszczss"
+         inkscape:connector-curvature="0"
+         id="path3516"
+         d="m 320.90633,434.00353 c -4.50028,0 -8.14847,3.69949 -8.14847,8.26307 0,1.56873 0.80432,3.6581 
1.17861,4.28277 0.3743,0.62468 6.82071,15.22767 6.82071,15.22767 0,0 6.63148,-14.45182 7.03171,-15.09118 
0.40022,-0.63936 1.26591,-2.79256 1.26591,-4.41926 0,-4.56358 -3.64819,-8.26307 -8.14847,-8.26307 z"
+         
style="fill:none;stroke:#5e9cf1;stroke-width:1.03512061;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
 />
+      <circle
+         r="18.75"
+         cy="396.11218"
+         cx="381.25"
+         transform="matrix(0.23144871,0,0,0.23144871,232.66651,350.69905)"
+         id="circle3518"
+         
style="fill:#ffffff;fill-opacity:1;stroke:#5e9cf1;stroke-width:4.32061148;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.69999999;stroke-opacity:1"
 />
+      <circle
+         r="18.75"
+         cy="396.11218"
+         cx="381.25"
+         
style="fill:#ffffff;fill-opacity:1;stroke:#0c438d;stroke-width:6.2157526;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.69999999;stroke-opacity:1"
+         id="circle3520"
+         transform="matrix(0.17705667,0,0,0.17705667,253.40348,372.2444)" />
+    </g>
+  </g>
+</svg>
diff --git a/data/icons/hicolor/scalable/actions/gps-markers-many.svg 
b/data/icons/hicolor/scalable/actions/gps-markers-many.svg
new file mode 100644
index 00000000..3b597564
--- /dev/null
+++ b/data/icons/hicolor/scalable/actions/gps-markers-many.svg
@@ -0,0 +1,477 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/";
+   xmlns:cc="http://creativecommons.org/ns#";
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+   xmlns:svg="http://www.w3.org/2000/svg";
+   xmlns="http://www.w3.org/2000/svg";
+   xmlns:xlink="http://www.w3.org/1999/xlink";
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd";
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape";
+   width="28.74"
+   height="38.98"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.91 r13725"
+   sodipodi:docname="gps-marker.svg">
+  <defs
+     id="defs4">
+    <linearGradient
+       id="linearGradient3887">
+      <stop
+         id="stop3889"
+         offset="0"
+         style="stop-color:#ff573f;stop-opacity:1;" />
+      <stop
+         id="stop3891"
+         offset="1"
+         style="stop-color:#b71111;stop-opacity:1;" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       id="linearGradient3857">
+      <stop
+         style="stop-color:#87b5f5;stop-opacity:1;"
+         offset="0"
+         id="stop3859" />
+      <stop
+         style="stop-color:#87b5f5;stop-opacity:0;"
+         offset="1"
+         id="stop3861" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       id="linearGradient3849">
+      <stop
+         style="stop-color:#ffffff;stop-opacity:1;"
+         offset="0"
+         id="stop3851" />
+      <stop
+         style="stop-color:#ffffff;stop-opacity:0;"
+         offset="1"
+         id="stop3853" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3827">
+      <stop
+         id="stop3829"
+         offset="0"
+         style="stop-color:#50a9ff;stop-opacity:1;" />
+      <stop
+         id="stop3831"
+         offset="1"
+         style="stop-color:#0034a9;stop-opacity:0.92490119;" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3821">
+      <stop
+         id="stop3823"
+         offset="0"
+         style="stop-color:#60aaf1;stop-opacity:1;" />
+      <stop
+         id="stop3825"
+         offset="1"
+         style="stop-color:#124cd1;stop-opacity:0.92490119;" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3787">
+      <stop
+         style="stop-color:#535353;stop-opacity:1;"
+         offset="0"
+         id="stop3789" />
+      <stop
+         style="stop-color:#ffffff;stop-opacity:0;"
+         offset="1"
+         id="stop3791" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3765">
+      <stop
+         style="stop-color:#23b3ff;stop-opacity:1;"
+         offset="0"
+         id="stop3767" />
+      <stop
+         style="stop-color:#124cd1;stop-opacity:0.92490119;"
+         offset="1"
+         id="stop3769" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3765"
+       id="linearGradient3771"
+       x1="381.42856"
+       y1="335.09586"
+       x2="381.42856"
+       y2="567.15851"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.13306152,0,0,0.13306152,324.43662,388.73998)" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3787"
+       id="radialGradient3793"
+       cx="374.25"
+       cy="464.11218"
+       fx="374.25"
+       fy="464.11218"
+       r="8.25"
+       gradientTransform="matrix(1,0,0,0.33333333,0,309.40812)"
+       gradientUnits="userSpaceOnUse" />
+    <filter
+       inkscape:collect="always"
+       id="filter3803"
+       x="-0.096096098"
+       width="1.1921922"
+       y="-0.2882883"
+       height="1.5765766"
+       color-interpolation-filters="sRGB">
+      <feGaussianBlur
+         inkscape:collect="always"
+         stdDeviation="0.66066066"
+         id="feGaussianBlur3805" />
+    </filter>
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3787"
+       id="radialGradient3843"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1,0,0,0.33333333,0,309.40812)"
+       cx="374.25"
+       cy="464.11218"
+       fx="374.25"
+       fy="464.11218"
+       r="8.25" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3765"
+       id="linearGradient3845"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.13306152,0,0,0.13306152,394.43662,388.73998)"
+       x1="381.42856"
+       y1="335.09586"
+       x2="381.42856"
+       y2="567.15851" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3849"
+       id="linearGradient3855"
+       x1="461.5"
+       y1="477.36218"
+       x2="462.5"
+       y2="434.36218"
+       gradientUnits="userSpaceOnUse" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3857"
+       id="linearGradient3863"
+       x1="444.95898"
+       y1="433.89029"
+       x2="444.95898"
+       y2="454.77341"
+       gradientUnits="userSpaceOnUse" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3849"
+       id="linearGradient3871"
+       x1="382.17749"
+       y1="377.47879"
+       x2="382.17749"
+       y2="414.47479"
+       gradientUnits="userSpaceOnUse" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3787"
+       id="radialGradient3883"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1,0,0,0.33333333,0,309.40812)"
+       cx="374.25"
+       cy="464.11218"
+       fx="374.25"
+       fy="464.11218"
+       r="8.25" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3887"
+       id="linearGradient3885"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.13306152,0,0,0.13306152,270.34295,388.73998)"
+       x1="381.42856"
+       y1="335.09586"
+       x2="381.42856"
+       y2="567.15851" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3787"
+       id="radialGradient3925"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1,0,0,0.33333333,0,309.40812)"
+       cx="374.25"
+       cy="464.11218"
+       fx="374.25"
+       fy="464.11218"
+       r="8.25" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3765"
+       id="linearGradient3927"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.13306152,0,0,0.13306152,324.43662,388.73998)"
+       x1="381.42856"
+       y1="335.09586"
+       x2="381.42856"
+       y2="567.15851" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3787"
+       id="radialGradient3941"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1,0,0,0.33333333,0,309.40812)"
+       cx="374.25"
+       cy="464.11218"
+       fx="374.25"
+       fy="464.11218"
+       r="8.25" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3765"
+       id="linearGradient3943"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.13306152,0,0,0.13306152,324.43662,388.73998)"
+       x1="381.42856"
+       y1="335.09586"
+       x2="381.42856"
+       y2="567.15851" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3787"
+       id="radialGradient3945"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1,0,0,0.33333333,0,309.40812)"
+       cx="374.25"
+       cy="464.11218"
+       fx="374.25"
+       fy="464.11218"
+       r="8.25" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3887"
+       id="linearGradient3947"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.13306152,0,0,0.13306152,270.34295,388.73998)"
+       x1="381.42856"
+       y1="335.09586"
+       x2="381.42856"
+       y2="567.15851" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3787"
+       id="radialGradient3506"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1,0,0,0.33333333,0,309.40812)"
+       cx="374.25"
+       cy="464.11218"
+       fx="374.25"
+       fy="464.11218"
+       r="8.25" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3887"
+       id="linearGradient3508"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.13306152,0,0,0.13306152,270.34295,388.73998)"
+       x1="381.42856"
+       y1="335.09586"
+       x2="381.42856"
+       y2="567.15851" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3787"
+       id="radialGradient3522"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1,0,0,0.33333333,0,309.40812)"
+       cx="374.25"
+       cy="464.11218"
+       fx="374.25"
+       fy="464.11218"
+       r="8.25" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3887"
+       id="linearGradient3524"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.13306152,0,0,0.13306152,270.34295,388.73998)"
+       x1="381.42856"
+       y1="335.09586"
+       x2="381.42856"
+       y2="567.15851" />
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="8"
+     inkscape:cx="-1.9262647"
+     inkscape:cy="18.625469"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:snap-global="false"
+     fit-margin-left="1"
+     units="px"
+     fit-margin-top="1"
+     fit-margin-right="1"
+     fit-margin-bottom="1"
+     inkscape:window-width="1440"
+     inkscape:window-height="836"
+     inkscape:window-x="0"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1">
+    <inkscape:grid
+       type="xygrid"
+       id="grid2987"
+       empspacing="5"
+       visible="true"
+       enabled="true"
+       snapvisiblegridlinesonly="true"
+       originx="-306.53522px"
+       originy="-506.68905px" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage"; />
+        <dc:title></dc:title>
+        <cc:license
+           rdf:resource="" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Ebene 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-306.53522,-506.69183)">
+    <g
+       id="g3035"
+       transform="translate(0,75.224057)">
+      <ellipse
+         ry="2.75"
+         rx="8.25"
+         cy="464.11218"
+         cx="374.25"
+         
style="opacity:0.58662612;fill:url(#radialGradient3945);fill-opacity:1;stroke:none;filter:url(#filter3803)"
+         id="path3873"
+         transform="matrix(1.3594635,0,0,1,-187.87288,1)" />
+      <path
+         
style="fill:url(#linearGradient3947);fill-opacity:1;stroke:#982f26;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         d="m 320.90633,432.9663 c -5.14414,0 -9.3143,4.17016 -9.3143,9.31432 0,1.76829 0.91939,4.12348 
1.34724,4.82763 0.42786,0.70414 7.79657,17.16494 7.79657,17.16494 0,0 7.58026,-16.29039 8.03775,-17.01109 
0.45749,-0.7207 1.44704,-3.14782 1.44704,-4.98148 0,-5.14416 -4.17016,-9.31432 -9.3143,-9.31432 z"
+         id="path3875"
+         inkscape:connector-curvature="0"
+         sodipodi:nodetypes="sszczss" />
+      <path
+         sodipodi:nodetypes="sszczss"
+         inkscape:connector-curvature="0"
+         id="path3877"
+         d="m 320.90633,434.00353 c -4.50028,0 -8.14847,3.69949 -8.14847,8.26307 0,1.56873 0.80432,3.6581 
1.17861,4.28277 0.3743,0.62468 6.82071,15.22767 6.82071,15.22767 0,0 6.63148,-14.45182 7.03171,-15.09118 
0.40022,-0.63936 1.26591,-2.79256 1.26591,-4.41926 0,-4.56358 -3.64819,-8.26307 -8.14847,-8.26307 z"
+         
style="fill:none;stroke:#e19089;stroke-width:1.03512061;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
 />
+      <circle
+         r="18.75"
+         cy="396.11218"
+         cx="381.25"
+         transform="matrix(0.23144871,0,0,0.23144871,232.66651,350.69905)"
+         id="path3879"
+         
style="fill:#ffffff;fill-opacity:1;stroke:#e19089;stroke-width:4.32061148;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.69999999;stroke-opacity:1"
 />
+      <circle
+         r="18.75"
+         cy="396.11218"
+         cx="381.25"
+         
style="fill:#ffffff;fill-opacity:1;stroke:#982f26;stroke-width:6.2157526;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.69999999;stroke-opacity:1"
+         id="path3881"
+         transform="matrix(0.17705667,0,0,0.17705667,253.40348,372.2444)" />
+    </g>
+    <g
+       transform="matrix(0.62943981,0,0,0.62943981,124.11992,251.22885)"
+       id="g3494">
+      <ellipse
+         transform="matrix(1.3594635,0,0,1,-187.87288,1)"
+         id="ellipse3496"
+         
style="opacity:0.58662612;fill:url(#radialGradient3506);fill-opacity:1;stroke:none;filter:url(#filter3803)"
+         cx="374.25"
+         cy="464.11218"
+         rx="8.25"
+         ry="2.75" />
+      <path
+         sodipodi:nodetypes="sszczss"
+         inkscape:connector-curvature="0"
+         id="path3498"
+         d="m 320.90633,432.9663 c -5.14414,0 -9.3143,4.17016 -9.3143,9.31432 0,1.76829 0.91939,4.12348 
1.34724,4.82763 0.42786,0.70414 7.79657,17.16494 7.79657,17.16494 0,0 7.58026,-16.29039 8.03775,-17.01109 
0.45749,-0.7207 1.44704,-3.14782 1.44704,-4.98148 0,-5.14416 -4.17016,-9.31432 -9.3143,-9.31432 z"
+         
style="fill:url(#linearGradient3508);fill-opacity:1;stroke:#982f26;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
 />
+      <path
+         
style="fill:none;stroke:#e19089;stroke-width:1.03512061;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         d="m 320.90633,434.00353 c -4.50028,0 -8.14847,3.69949 -8.14847,8.26307 0,1.56873 0.80432,3.6581 
1.17861,4.28277 0.3743,0.62468 6.82071,15.22767 6.82071,15.22767 0,0 6.63148,-14.45182 7.03171,-15.09118 
0.40022,-0.63936 1.26591,-2.79256 1.26591,-4.41926 0,-4.56358 -3.64819,-8.26307 -8.14847,-8.26307 z"
+         id="path3500"
+         inkscape:connector-curvature="0"
+         sodipodi:nodetypes="sszczss" />
+      <circle
+         
style="fill:#ffffff;fill-opacity:1;stroke:#e19089;stroke-width:4.32061148;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.69999999;stroke-opacity:1"
+         id="circle3502"
+         transform="matrix(0.23144871,0,0,0.23144871,232.66651,350.69905)"
+         cx="381.25"
+         cy="396.11218"
+         r="18.75" />
+      <circle
+         transform="matrix(0.17705667,0,0,0.17705667,253.40348,372.2444)"
+         id="circle3504"
+         
style="fill:#ffffff;fill-opacity:1;stroke:#982f26;stroke-width:6.2157526;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.69999999;stroke-opacity:1"
+         cx="381.25"
+         cy="396.11218"
+         r="18.75" />
+    </g>
+    <g
+       id="g3510"
+       transform="matrix(0.62943981,0,0,0.62943981,113.99492,248.22885)">
+      <ellipse
+         ry="2.75"
+         rx="8.25"
+         cy="464.11218"
+         cx="374.25"
+         
style="opacity:0.58662612;fill:url(#radialGradient3522);fill-opacity:1;stroke:none;filter:url(#filter3803)"
+         id="ellipse3512"
+         transform="matrix(1.3594635,0,0,1,-187.87288,1)" />
+      <path
+         
style="fill:url(#linearGradient3524);fill-opacity:1;stroke:#982f26;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         d="m 320.90633,432.9663 c -5.14414,0 -9.3143,4.17016 -9.3143,9.31432 0,1.76829 0.91939,4.12348 
1.34724,4.82763 0.42786,0.70414 7.79657,17.16494 7.79657,17.16494 0,0 7.58026,-16.29039 8.03775,-17.01109 
0.45749,-0.7207 1.44704,-3.14782 1.44704,-4.98148 0,-5.14416 -4.17016,-9.31432 -9.3143,-9.31432 z"
+         id="path3514"
+         inkscape:connector-curvature="0"
+         sodipodi:nodetypes="sszczss" />
+      <path
+         sodipodi:nodetypes="sszczss"
+         inkscape:connector-curvature="0"
+         id="path3516"
+         d="m 320.90633,434.00353 c -4.50028,0 -8.14847,3.69949 -8.14847,8.26307 0,1.56873 0.80432,3.6581 
1.17861,4.28277 0.3743,0.62468 6.82071,15.22767 6.82071,15.22767 0,0 6.63148,-14.45182 7.03171,-15.09118 
0.40022,-0.63936 1.26591,-2.79256 1.26591,-4.41926 0,-4.56358 -3.64819,-8.26307 -8.14847,-8.26307 z"
+         
style="fill:none;stroke:#e19089;stroke-width:1.03512061;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
 />
+      <circle
+         r="18.75"
+         cy="396.11218"
+         cx="381.25"
+         transform="matrix(0.23144871,0,0,0.23144871,232.66651,350.69905)"
+         id="circle3518"
+         
style="fill:#ffffff;fill-opacity:1;stroke:#e19089;stroke-width:4.32061148;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.69999999;stroke-opacity:1"
 />
+      <circle
+         r="18.75"
+         cy="396.11218"
+         cx="381.25"
+         
style="fill:#ffffff;fill-opacity:1;stroke:#982f26;stroke-width:6.2157526;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.69999999;stroke-opacity:1"
+         id="circle3520"
+         transform="matrix(0.17705667,0,0,0.17705667,253.40348,372.2444)" />
+    </g>
+  </g>
+</svg>
diff --git a/src/MapWidget.vala b/src/MapWidget.vala
index a8b018aa..a803e4b9 100644
--- a/src/MapWidget.vala
+++ b/src/MapWidget.vala
@@ -4,97 +4,281 @@
  * See the COPYING file in this distribution.
  */
 
-private class PositionMarker : Object {
-    private MapWidget map_widget;
+private interface PositionMarker : Object {
+    public abstract Champlain.Marker champlain_marker { get; protected set; }
+    public abstract bool highlighted { get; set; }
+    public abstract bool selected { get; set; }
+}
 
-    protected PositionMarker.from_group(MapWidget map_widget) {
-        this.map_widget = map_widget;
+private abstract class AbstractPositionMarker : Object, PositionMarker {
+    private bool _selected = false;
+    protected MapWidget map_widget;
+
+    public Champlain.Marker champlain_marker { get; protected set; }
+
+    protected abstract Gee.Collection<DataViewPositionMarker> data_view_position_markers { owned get; }
+
+    public bool highlighted {
+        get { return champlain_marker.get_selected(); }
+        set {
+            if (value || !_selected)
+                champlain_marker.set_selected(value);
+        }
+    }
+    public bool selected {
+        get {
+            return _selected;
+        }
+        set {
+            _selected = value;
+            champlain_marker.set_selected(value);
+        }
     }
 
-    public PositionMarker(MapWidget map_widget, DataView view, Champlain.Marker marker) {
-        this.map_widget = map_widget;
-        this.view = view;
-        marker.selectable = true;
-        marker.button_release_event.connect ((event) => {
-            if (event.button > 1)
+    protected void bind_mouse_events() {
+        champlain_marker.button_release_event.connect ((event) => {
+            if (event.button > 1 || _selected)
                 return true;
-            map_widget.select_data_view(this);
+            champlain_marker.selected = true;
+            map_widget.select_data_views(data_view_position_markers);
             return true;
         });
-        marker.enter_event.connect ((event) => {
-            map_widget.highlight_data_view(this);
+        champlain_marker.enter_event.connect ((event) => {
+            if (!_selected)
+                champlain_marker.selected = true;
+            map_widget.highlight_data_views(data_view_position_markers);
             return true;
         });
-        marker.leave_event.connect ((event) => {
-            map_widget.unhighlight_data_view(this);
+        champlain_marker.leave_event.connect ((event) => {
+            if (!_selected)
+                champlain_marker.selected = false;
+            map_widget.unhighlight_data_views(data_view_position_markers);
             return true;
         });
-        this.marker = marker;
     }
+}
 
-    public bool selected {
-        get {
-            return marker.get_selected();
-        }
-        set {
-            marker.set_selected(value);
-        }
+private class DataViewPositionMarker : AbstractPositionMarker {
+    private Gee.ArrayList<DataViewPositionMarker> _data_view_position_markers;
+
+    protected override Gee.Collection<DataViewPositionMarker> data_view_position_markers {
+        owned get { return _data_view_position_markers.read_only_view; }
     }
 
-    public Champlain.Marker marker { get; protected set; }
     // Geo lookup
     // public string location_country { get; set; }
     // public string location_city { get; set; }
-    public unowned DataView view { get; protected set; }
+    public weak DataView view { get; protected set; }
+
+    public DataViewPositionMarker(MapWidget map_widget, DataView view, Champlain.Marker champlain_marker) {
+        this.map_widget = map_widget;
+        this.view = view;
+        champlain_marker.selectable = true;
+        var list = new Gee.ArrayList<DataViewPositionMarker>();
+        list.add(this);
+        this._data_view_position_markers = list;
+        this.champlain_marker = champlain_marker;
+        bind_mouse_events();
+    }
 }
 
-private class MarkerGroup : PositionMarker {
-    private Gee.Set<PositionMarker> markers = new Gee.HashSet<PositionMarker>();
-    public MarkerGroup(MapWidget map_widget, PositionMarker first_marker) {
-        base.from_group(map_widget);
-        markers.add(first_marker);
-        // use the first markers internal texture as the group's
-        marker = first_marker.marker;
-        view = first_marker.view;
+private class MarkerGroup : AbstractPositionMarker {
+    private Gee.Collection<DataViewPositionMarker> _data_view_position_markers =
+        new Gee.LinkedList<DataViewPositionMarker>();
+    private Gee.Collection<PositionMarker> _position_markers = new Gee.LinkedList<PositionMarker>();
+    private Champlain.BoundingBox bbox = new Champlain.BoundingBox();
+
+    protected override Gee.Collection<DataViewPositionMarker> data_view_position_markers {
+        owned get { return _data_view_position_markers.read_only_view; }
+    }
+
+    public Gee.Collection<PositionMarker> position_markers {
+        owned get { return _position_markers.read_only_view; }
+    }
+
+    public MarkerGroup(MapWidget map_widget, Champlain.Marker champlain_marker) {
+        this.map_widget = map_widget;
+        champlain_marker.selectable = true;
+        this.champlain_marker = champlain_marker;
+        bind_mouse_events();
+    }
+
+    public void add_position_marker(PositionMarker marker) {
+        var data_view_position_marker = marker as DataViewPositionMarker;
+        if (data_view_position_marker != null)
+            _data_view_position_markers.add(data_view_position_marker);
+        var new_champlain_marker = marker.champlain_marker;
+        bbox.extend(new_champlain_marker.latitude, new_champlain_marker.longitude);
+        double lat, lon;
+        bbox.get_center(out lat, out lon);
+        champlain_marker.set_location(lat, lon);
+        _position_markers.add(marker);
+    }
+}
+
+private class MarkerGroupRaster : Object {
+    private const long MARKER_GROUP_RASTER_WIDTH_PX = 30l;
+    private const long MARKER_GROUP_RASTER_HEIGHT_PX = 30l;
+
+    private MapWidget map_widget;
+    private Champlain.View map_view;
+    private Champlain.MarkerLayer marker_layer;
+
+    // position_markers_tree is a two-dimensional tree for grouping position
+    // markers indexed by x (outer tree) and y (inner tree) raster coordinates.
+    // It maps coordinates to the PositionMarker (DataViewMarker or MarkerGroup)
+    // corresponding to them.
+    // If either raster index keys are empty, there is no marker within the
+    // raster cell. If both exist there are two possibilities:
+    // (1) the value is a MarkerGroup which means that multiple markers are
+    // grouped together, or (2) the value is a PositionMarker (but not a
+    // MarkerGroup) which means that there is exactly one marker in the raster
+    // cell. The tree is recreated every time the zoom level changes.
+    private Gee.TreeMap<long, Gee.TreeMap<long, weak PositionMarker?>?> position_markers_tree =
+        new Gee.TreeMap<long, Gee.TreeMap<long, weak PositionMarker?>?>();
+    // The marker groups collection keeps track of and owns all PositionMarkers including the marker groups
+    private Gee.Map<DataView, weak PositionMarker> data_view_map = new Gee.HashMap<DataView, weak 
PositionMarker>();
+    private Gee.Set<PositionMarker> position_markers = new Gee.HashSet<PositionMarker>();
+
+    public MarkerGroupRaster(MapWidget map_widget, Champlain.View map_view, Champlain.MarkerLayer 
marker_layer) {
+        this.map_widget = map_widget;
+        this.map_view = map_view;
+        this.marker_layer = marker_layer;
+        map_widget.zoom_changed.connect(regroup);
+    }
+
+    public void clear() {
+        data_view_map.clear();
+        position_markers_tree.clear();
+        position_markers.clear();
+    }
+
+    public weak PositionMarker? find_position_marker(DataView data_view) {
+        if (!data_view_map.has_key(data_view))
+            return null;
+        weak PositionMarker? m;
+        lock (position_markers) {
+            m = data_view_map.get(data_view);
+        }
+        return m;
+    }
+
+    public bool has_markers() {
+        return !position_markers.is_empty;
+    }
+
+    public void rasterize_marker(PositionMarker position_marker, bool already_on_map=false) {
+        var data_view_position_marker = position_marker as DataViewPositionMarker;
+        var champlain_marker = position_marker.champlain_marker;
+        long x, y;
+
+        lock (position_markers) {
+            rasterize_coords(champlain_marker.longitude, champlain_marker.latitude, out x, out y);
+            var yg = position_markers_tree.get(x);
+            if (yg == null) {
+                yg = new Gee.TreeMap<long, weak PositionMarker?>();
+                position_markers_tree.set(x, yg);
+            }
+            var cell = yg.get(y);
+            if (cell == null) {
+                // first marker in this raster cell
+                yg.set(y, position_marker);
+                position_markers.add(position_marker);
+                if (!already_on_map)
+                    marker_layer.add_marker(position_marker.champlain_marker);
+                if (data_view_position_marker != null)
+                    data_view_map.set(data_view_position_marker.view, position_marker);
+
+            } else {
+                var marker_group = cell as MarkerGroup;
+                if (marker_group == null) {
+                    // single marker already occupies raster cell: create new group
+                    GpsCoords rasterized_gps_coords = GpsCoords() {
+                        has_gps = 1,
+                        longitude = map_view.x_to_longitude(x),
+                        latitude = map_view.y_to_latitude(y)
+                    };
+                    marker_group = map_widget.create_marker_group(rasterized_gps_coords);
+                    marker_group.add_position_marker(cell);
+                    if (cell is DataViewPositionMarker)
+                        data_view_map.set(((DataViewPositionMarker) cell).view, marker_group);
+                    yg.set(y, marker_group);
+                    position_markers.add(marker_group);
+                    position_markers.remove(cell);
+                    marker_layer.add_marker(marker_group.champlain_marker);
+                    marker_layer.remove_marker(cell.champlain_marker);
+                }
+                // group already exists, add new marker to it
+                marker_group.add_position_marker(position_marker);
+                if (already_on_map)
+                    marker_layer.remove_marker(position_marker.champlain_marker);
+                if (data_view_position_marker != null)
+                    data_view_map.set(data_view_position_marker.view, marker_group);
+            }
+        }
     }
-    public void add_marker(PositionMarker marker) {
-        markers.add(marker);
+
+    private void rasterize_coords(double longitude, double latitude, out long x, out long y) {
+        x = (Math.lround(map_view.longitude_to_x(longitude) / MARKER_GROUP_RASTER_WIDTH_PX)) *
+            MARKER_GROUP_RASTER_WIDTH_PX + (MARKER_GROUP_RASTER_WIDTH_PX / 2);
+        y = (Math.lround(map_view.latitude_to_y(latitude) / MARKER_GROUP_RASTER_HEIGHT_PX)) *
+            MARKER_GROUP_RASTER_HEIGHT_PX + (MARKER_GROUP_RASTER_HEIGHT_PX / 2);
     }
-    public Gee.Set<PositionMarker> get_markers() {
-        return markers;
+
+    private void regroup() {
+        lock (position_markers) {
+            var position_markers_current = (owned) position_markers;
+            position_markers = new Gee.HashSet<PositionMarker>();
+            position_markers_tree.clear();
+
+            foreach (var pm in position_markers_current) {
+                var marker_group = pm as MarkerGroup;
+                if (marker_group != null) {
+                    marker_layer.remove_marker(marker_group.champlain_marker);
+                    foreach (var position_marker in marker_group.position_markers) {
+                        rasterize_marker(position_marker, false);
+                    }
+                } else {
+                    rasterize_marker(pm, true);
+                }
+            }
+            position_markers_current = null;
+        }
     }
 }
 
 private class MapWidget : Gtk.Bin {
     private const uint DEFAULT_ZOOM_LEVEL = 8;
-    private const long MARKER_GROUP_RASTER_WIDTH = 30l;
 
     private static MapWidget instance = null;
 
     private GtkChamplain.Embed gtk_champlain_widget = new GtkChamplain.Embed();
     private Champlain.View map_view = null;
-    private uint last_zoom_level = DEFAULT_ZOOM_LEVEL;
     private Champlain.Scale map_scale = new Champlain.Scale();
     private Champlain.MarkerLayer marker_layer = new Champlain.MarkerLayer();
     public bool map_edit_lock { get; set; }
-    private Gee.Map<DataView, PositionMarker> position_markers =
-        new Gee.HashMap<DataView, PositionMarker>();
-    private Gee.TreeMap<long, Gee.TreeMap<long, MarkerGroup>> marker_groups_tree =
-        new Gee.TreeMap<long, Gee.TreeMap<long, MarkerGroup>>();
-    private Gee.Collection<MarkerGroup> marker_groups = new Gee.LinkedList<MarkerGroup>();
-    private unowned Page page = null;
+    private MarkerGroupRaster marker_group_raster = null;
+    private weak Page page = null;
     private Clutter.Image? map_edit_locked_image;
     private Clutter.Image? map_edit_unlocked_image;
     private Clutter.Actor map_edit_lock_button = new Clutter.Actor();
 
+    public const float MARKER_IMAGE_HORIZONTAL_PIN_RATIO = 0.5f;
+    public const float MARKER_IMAGE_VERTICAL_PIN_RATIO = 0.825f;
     public float marker_image_width { get; private set; }
     public float marker_image_height { get; private set; }
+    public float marker_group_image_width { get; private set; }
+    public float marker_group_image_height { get; private set; }
     public float map_edit_lock_image_width { get; private set; }
     public float map_edit_lock_image_height { get; private set; }
     public Clutter.Image? marker_image { get; private set; }
     public Clutter.Image? marker_selected_image { get; private set; }
+    public Clutter.Image? marker_group_image { get; private set; }
+    public Clutter.Image? marker_group_selected_image { get; private set; }
     public const Clutter.Color marker_point_color = { 10, 10, 255, 192 };
 
+    public signal void zoom_changed();
+
     private MapWidget() {
         setup_map();
         add(gtk_champlain_widget);
@@ -126,54 +310,23 @@ private class MapWidget : Gtk.Bin {
 
     public void clear() {
         marker_layer.remove_all();
-        marker_groups_tree.clear();
-        marker_groups.clear();
-        position_markers.clear();
+        marker_group_raster.clear();
     }
 
-    public void add_position_marker(DataView view) {
+    public void add_data_view(DataView view) {
         DataSource view_source = view.get_source();
-        if (!(view_source is Positionable)) {
+        if (!(view_source is Positionable))
             return;
-        }
         Positionable p = (Positionable) view_source;
         GpsCoords gps_coords = p.get_gps_coords();
-        if (gps_coords.has_gps <= 0) {
+        if (gps_coords.has_gps <= 0)
             return;
-        }
-
-        // rasterize coords
-        long x = (long)(map_view.longitude_to_x(gps_coords.longitude) / MARKER_GROUP_RASTER_WIDTH);
-        long y = (long)(map_view.latitude_to_y(gps_coords.latitude) / MARKER_GROUP_RASTER_WIDTH);
         PositionMarker position_marker = create_position_marker(view);
-        var yg = marker_groups_tree.get(x);
-        if (yg == null) {
-            // y group doesn't exist, initialize it
-            yg = new Gee.TreeMap<long, MarkerGroup>();
-            var mg = new MarkerGroup(this, position_marker);
-            yg.set(y, mg);
-            marker_groups.add(mg);
-            marker_groups_tree.set(x, yg);
-            add_marker(mg.marker);
-        } else {
-            var mg = yg.get(y);
-            if (mg == null) {
-                // first marker in this group
-                mg = new MarkerGroup(this, position_marker);
-                yg.set(y, mg);
-                marker_groups.add(mg);
-                add_marker(mg.marker);
-            } else {
-                // marker group already exists
-                mg.add_marker(position_marker);
-            }
-        }
-
-        position_markers.set(view, position_marker);
+        marker_group_raster.rasterize_marker(position_marker);
     }
 
     public void show_position_markers() {
-        if (!position_markers.is_empty) {
+        if (!marker_group_raster.has_markers()) {
             if (map_view.get_zoom_level() < DEFAULT_ZOOM_LEVEL) {
                 map_view.set_zoom_level(DEFAULT_ZOOM_LEVEL);
             }
@@ -182,62 +335,81 @@ private class MapWidget : Gtk.Bin {
         }
     }
 
-    public void select_data_view(PositionMarker m) {
-        ViewCollection page_view = null;
-        if (page != null)
-            page_view = page.get_view();
-        if (page_view != null && m.view is CheckerboardItem) {
+    public void select_data_views(Gee.Collection<DataViewPositionMarker> ms) {
+        if (page == null)
+            return;
+
+        ViewCollection page_view = page.get_view();
+        if (page_view != null) {
             Marker marked = page_view.start_marking();
-            marked.mark(m.view);
+            foreach (var m in ms) {
+                if (m.view is CheckerboardItem) {
+                    marked.mark(m.view);
+                }
+            }
             page_view.unselect_all();
             page_view.select_marked(marked);
         }
     }
 
-    public void highlight_data_view(PositionMarker m) {
-        if (page != null && m.view is CheckerboardItem) {
-            CheckerboardItem item = (CheckerboardItem) m.view;
-
-            // if item is in any way out of view, scroll to it
-            Gtk.Adjustment vadj = page.get_vadjustment();
-
-            if (!(get_adjustment_relation(vadj, item.allocation.y) == AdjustmentRelation.IN_RANGE
-                && (get_adjustment_relation(vadj, item.allocation.y + item.allocation.height) == 
AdjustmentRelation.IN_RANGE))) {
+    public void highlight_data_views(Gee.Collection<DataViewPositionMarker> ms) {
+        if (page == null)
+            return;
 
-                // scroll to see the new item
-                int top = 0;
-                if (item.allocation.y < vadj.get_value()) {
-                    top = item.allocation.y;
-                    top -= CheckerboardLayout.ROW_GUTTER_PADDING / 2;
-                } else {
-                    top = item.allocation.y + item.allocation.height - (int) vadj.get_page_size();
-                    top += CheckerboardLayout.ROW_GUTTER_PADDING / 2;
+        bool did_adjust_view = false;
+        foreach (var m in ms) {
+            if (m.view is CheckerboardItem) {
+                CheckerboardItem item = (CheckerboardItem) m.view;
+
+                if (!did_adjust_view) {
+                    // if first item is in any way out of view, scroll to it
+                    Gtk.Adjustment vadj = page.get_vadjustment();
+
+                    if (!(get_adjustment_relation(vadj, item.allocation.y) == AdjustmentRelation.IN_RANGE
+                        && (get_adjustment_relation(vadj, item.allocation.y + item.allocation.height) == 
AdjustmentRelation.IN_RANGE))) {
+
+                        // scroll to see the new item
+                        int top = 0;
+                        if (item.allocation.y < vadj.get_value()) {
+                            top = item.allocation.y;
+                            top -= CheckerboardLayout.ROW_GUTTER_PADDING / 2;
+                        } else {
+                            top = item.allocation.y + item.allocation.height - (int) vadj.get_page_size();
+                            top += CheckerboardLayout.ROW_GUTTER_PADDING / 2;
+                        }
+
+                        vadj.set_value(top);
+                    }
+                    did_adjust_view = true;
                 }
-
-                vadj.set_value(top);
+                item.brighten();
             }
-            item.brighten();
         }
     }
 
-    public void unhighlight_data_view(PositionMarker m) {
-        if (page != null && m.view is CheckerboardItem) {
-            CheckerboardItem item = (CheckerboardItem) m.view;
-            item.unbrighten();
+    public void unhighlight_data_views(Gee.Collection<DataViewPositionMarker> ms) {
+        if (page == null)
+            return;
+
+        foreach (var m in ms) {
+            if (m.view is CheckerboardItem) {
+                CheckerboardItem item = (CheckerboardItem) m.view;
+                item.unbrighten();
+            }
         }
     }
 
     public void highlight_position_marker(DataView v) {
-        PositionMarker? m = position_markers.get(v);
-        if (m != null) {
-            m.selected = true;
+        weak PositionMarker? position_marker = marker_group_raster.find_position_marker(v);
+        if (position_marker != null) {
+            position_marker.highlighted = true;
         }
     }
 
     public void unhighlight_position_marker(DataView v) {
-        PositionMarker? m = position_markers.get(v);
-        if (m != null) {
-            m.selected = false;
+        weak PositionMarker? position_marker = marker_group_raster.find_position_marker(v);
+        if (position_marker != null) {
+            position_marker.highlighted = false;
         }
     }
 
@@ -278,7 +450,10 @@ private class MapWidget : Gtk.Bin {
         map_view.bin_layout_add(map_scale, Clutter.BinAlignment.START, Clutter.BinAlignment.END);
 
         map_view.set_zoom_on_double_click(false);
-        map_view.layer_relocated.connect(map_relocated_handler);
+        map_view.notify.connect((o, p) => {
+            if (p.name == "zoom-level")
+                zoom_changed();
+        });
 
         Gtk.TargetEntry[] dnd_targets = {
             LibraryWindow.DND_TARGET_ENTRIES[LibraryWindow.TargetType.URI_LIST],
@@ -289,6 +464,8 @@ private class MapWidget : Gtk.Bin {
         button_press_event.connect(map_zoom_handler);
         set_size_request(200, 200);
 
+        marker_group_raster = new MarkerGroupRaster(this, map_view, marker_layer);
+
         // Load icons
         float w, h;
         marker_image = Resources.get_icon_as_clutter_image(
@@ -297,6 +474,12 @@ private class MapWidget : Gtk.Bin {
         marker_image_height = h;
         marker_selected_image = Resources.get_icon_as_clutter_image(
                 Resources.ICON_GPS_MARKER_SELECTED, out w, out h);
+        marker_group_image = Resources.get_icon_as_clutter_image(
+                Resources.ICON_GPS_GROUP_MARKER, out w, out h);
+        marker_group_image_width = w;
+        marker_group_image_height = h;
+        marker_group_selected_image = Resources.get_icon_as_clutter_image(
+                Resources.ICON_GPS_GROUP_MARKER_SELECTED, out w, out h);
         map_edit_locked_image = Resources.get_icon_as_clutter_image(
                 Resources.ICON_MAP_EDIT_LOCKED, out w, out h);
         map_edit_unlocked_image = Resources.get_icon_as_clutter_image(
@@ -312,10 +495,9 @@ private class MapWidget : Gtk.Bin {
         }
     }
 
-    private PositionMarker create_position_marker(DataView view) {
-        DataSource data_source = view.get_source();
-        Positionable p = (Positionable) data_source;
-        GpsCoords gps_coords = p.get_gps_coords();
+    private Champlain.Marker create_champlain_marker(GpsCoords gps_coords, Clutter.Image? marker_image,
+                                                     Clutter.Image? marker_selected_image,
+                                                     float marker_image_width, float marker_image_height) {
         assert(gps_coords.has_gps > 0);
         Champlain.Marker champlain_marker;
         if (marker_image == null) {
@@ -325,6 +507,9 @@ private class MapWidget : Gtk.Bin {
             champlain_marker = new Champlain.Marker();
             champlain_marker.set_content(marker_image);
             champlain_marker.set_size(marker_image_width, marker_image_height);
+            champlain_marker.set_translation(-marker_image_width * MARKER_IMAGE_HORIZONTAL_PIN_RATIO,
+                                             -marker_image_height * MARKER_IMAGE_VERTICAL_PIN_RATIO, 0);
+            //champlain_marker.set_pivot_point(MARKER_IMAGE_HORIZONTAL_PIN_RATIO, 
MARKER_IMAGE_VERTICAL_PIN_RATIO);
             champlain_marker.notify.connect((o, p) => {
                 Champlain.Marker? m = o as Champlain.Marker;
                 if (p.name == "selected")
@@ -333,11 +518,23 @@ private class MapWidget : Gtk.Bin {
         }
         champlain_marker.set_pivot_point(0.5f, 0.5f); // set center of marker
         champlain_marker.set_location(gps_coords.latitude, gps_coords.longitude);
-        return new PositionMarker(this, view, champlain_marker);
+        return champlain_marker;
     }
 
-    private void add_marker(Champlain.Marker marker) {
-        marker_layer.add_marker(marker);
+    private DataViewPositionMarker create_position_marker(DataView view) {
+        // TODO: store markers in map to only create them once
+        DataSource data_source = view.get_source();
+        Positionable p = (Positionable) data_source;
+        GpsCoords gps_coords = p.get_gps_coords();
+        Champlain.Marker champlain_marker = create_champlain_marker(gps_coords, marker_image,
+            marker_selected_image, marker_image_width, marker_image_height);
+        return new DataViewPositionMarker(this, view, champlain_marker);
+    }
+
+    internal MarkerGroup create_marker_group(GpsCoords gps_coords) {
+        Champlain.Marker champlain_marker = create_champlain_marker(gps_coords, marker_group_image,
+            marker_group_selected_image, marker_group_image_width, marker_group_image_height);
+        return new MarkerGroup(this, champlain_marker);
     }
 
     private bool map_zoom_handler(Gdk.EventButton event) {
@@ -357,50 +554,6 @@ private class MapWidget : Gtk.Bin {
         return false;
     }
 
-    private void map_relocated_handler() {
-        uint new_zoom_level = map_view.get_zoom_level();
-        if (last_zoom_level != new_zoom_level) {
-            rezoom();
-            last_zoom_level = new_zoom_level;
-        }
-    }
-
-    private void rezoom() {
-        marker_groups_tree.clear();
-        Gee.Collection<MarkerGroup> marker_groups_new = new Gee.LinkedList<MarkerGroup>();
-        foreach (var marker_group in marker_groups) {
-            marker_layer.remove_marker(marker_group.marker);
-            foreach (var position_marker in marker_group.get_markers()) {
-                // rasterize coords
-                long x = (long)(map_view.longitude_to_x(position_marker.marker.longitude) / 
MARKER_GROUP_RASTER_WIDTH);
-                long y = (long)(map_view.latitude_to_y(position_marker.marker.latitude) / 
MARKER_GROUP_RASTER_WIDTH);
-                var yg = marker_groups_tree.get(x);
-                if (yg == null) {
-                    // y group doesn't exist, initialize it
-                    yg = new Gee.TreeMap<long, MarkerGroup>();
-                    var mg = new MarkerGroup(this, position_marker);
-                    yg.set(y, mg);
-                    marker_groups_new.add(mg);
-                    marker_groups_tree.set(x, yg);
-                    add_marker(mg.marker);
-                } else {
-                    var mg = yg.get(y);
-                    if (mg == null) {
-                        // first marker -> create new group
-                        mg = new MarkerGroup(this, position_marker);
-                        yg.set(y, mg);
-                        marker_groups_new.add(mg);
-                        add_marker(mg.marker);
-                    } else {
-                        // marker group already exists
-                        mg.add_marker(position_marker);
-                    }
-                }
-            }
-        }
-        marker_groups = marker_groups_new;
-    }
-
     private bool internal_drop_received(Gee.List<MediaSource> media, double lat, double lon) {
         if (map_edit_lock) {
             return false;
diff --git a/src/Properties.vala b/src/Properties.vala
index c7a094a2..09ccd46c 100644
--- a/src/Properties.vala
+++ b/src/Properties.vala
@@ -268,7 +268,7 @@ private class BasicProperties : Properties {
             }
             end_time = start_time;
         }
-        map_widget.add_position_marker(view);
+        map_widget.add_data_view(view);
 
     }
 
@@ -334,7 +334,7 @@ private class BasicProperties : Properties {
 
                 video_count++;
             }
-            map_widget.add_position_marker(view);
+            map_widget.add_data_view(view);
         }
     }
 
diff --git a/src/Resources.vala b/src/Resources.vala
index c562d1d9..5f324b2a 100644
--- a/src/Resources.vala
+++ b/src/Resources.vala
@@ -84,6 +84,8 @@ along with Shotwell; if not, write to the Free Software Foundation, Inc.,
     public const int ICON_ZOOM_SCALE = 16;
     public const string ICON_GPS_MARKER = "gps-marker.svg";
     public const string ICON_GPS_MARKER_SELECTED = "gps-marker-selected.svg";
+    public const string ICON_GPS_GROUP_MARKER = "gps-markers-many.svg";
+    public const string ICON_GPS_GROUP_MARKER_SELECTED = "gps-markers-many-selected.svg";
     public const string ICON_MAP_EDIT_LOCKED = "map-edit-locked.svg";
     public const string ICON_MAP_EDIT_UNLOCKED = "map-edit-unlocked.svg";
 


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]