[shotwell] map: Initial Map-Widget



commit 35346fc1087169d2b22923ac880a50eba0779717
Author: Andreas Brauchli <a brauchli elementarea net>
Date:   Thu Jul 14 13:46:00 2016 +0200

    map: Initial Map-Widget
    
    Initial map widget support:
    - Add map widget to properties
    - Increase the DB schema to v21:
      Add position (gps) metadata to DB
    - marker vector graphics courtesy of Alexander Wilms licensed
      CC0 https://creativecommons.org/publicdomain/zero/1.0/

 .../scalable/actions/gps-marker-selected.svg       | 427 +++++++++++++++++++++
 data/icons/hicolor/scalable/actions/gps-marker.svg | 370 ++++++++++++++++++
 src/MapWidget.vala                                 | 397 +++++++++++++++++++
 src/MetadataWriter.vala                            |  10 +-
 src/Page.vala                                      |   7 +-
 src/Photo.vala                                     |  57 ++-
 src/Properties.vala                                |  42 +-
 src/Resources.vala                                 |   2 +
 src/core/SourceInterfaces.vala                     |  15 +
 src/db/DatabaseTable.vala                          |  15 +-
 src/db/Db.vala                                     |  18 +-
 src/db/PhotoTable.vala                             |  82 +++-
 src/library/LibraryWindow.vala                     |   9 +-
 src/main.vala                                      |   2 +-
 src/photos/PhotoMetadata.vala                      |  22 +-
 src/util/misc.vala                                 |   4 +-
 16 files changed, 1428 insertions(+), 51 deletions(-)
---
diff --git a/data/icons/hicolor/scalable/actions/gps-marker-selected.svg 
b/data/icons/hicolor/scalable/actions/gps-marker-selected.svg
new file mode 100644
index 00000000..4f65c1db
--- /dev/null
+++ b/data/icons/hicolor/scalable/actions/gps-marker-selected.svg
@@ -0,0 +1,427 @@
+<?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.742239"
+   height="38.981468"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.48.3.1 r9886"
+   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-3"
+       id="radialGradient3909"
+       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
+       id="linearGradient3787-3">
+      <stop
+         style="stop-color:#535353;stop-opacity:1;"
+         offset="0"
+         id="stop3789-9" />
+      <stop
+         style="stop-color:#ffffff;stop-opacity:0;"
+         offset="1"
+         id="stop3791-3" />
+    </linearGradient>
+    <filter
+       inkscape:collect="always"
+       id="filter3803-2"
+       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-5" />
+    </filter>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3765-3"
+       id="linearGradient3911"
+       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" />
+    <linearGradient
+       id="linearGradient3765-3">
+      <stop
+         style="stop-color:#23b3ff;stop-opacity:1;"
+         offset="0"
+         id="stop3767-3" />
+      <stop
+         style="stop-color:#124cd1;stop-opacity:0.92490119;"
+         offset="1"
+         id="stop3769-4" />
+    </linearGradient>
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="4"
+     inkscape:cx="-23.118743"
+     inkscape:cy="60.73634"
+     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="844"
+     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.5341px"
+       originy="-506.68832px" />
+  </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.5341,-506.6911)">
+    <g
+       transform="translate(-76.094785,7.3864159e-4)"
+       id="g3042">
+      <g
+         transform="translate(22,75.224057)"
+         id="g3895">
+        <path
+           sodipodi:type="arc"
+           
style="opacity:0.58662612;fill:url(#radialGradient3909);fill-opacity:1;stroke:none;filter:url(#filter3803-2)"
+           id="path3785"
+           sodipodi:cx="374.25"
+           sodipodi:cy="464.11218"
+           sodipodi:rx="8.25"
+           sodipodi:ry="2.75"
+           d="m 382.5,464.11218 c 0,1.51879 -3.69365,2.75 -8.25,2.75 -4.55635,0 -8.25,-1.23121 -8.25,-2.75 
0,-1.51878 3.69365,-2.75 8.25,-2.75 4.55635,0 8.25,1.23122 8.25,2.75 z"
+           transform="matrix(1.3594635,0,0,1,-133.77921,1)" />
+        <path
+           
style="fill:url(#linearGradient3911);fill-opacity:1;stroke:#0b3e83;stroke-width:1;stroke-miterlimit:4;stroke-opacity:0.96862745;stroke-dasharray:none"
+           d="m 375,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="path2985"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="sszczss" />
+        <path
+           sodipodi:nodetypes="sszczss"
+           inkscape:connector-curvature="0"
+           id="path3775"
+           d="m 375,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:#69a3f2;stroke-width:1.03512061;stroke-miterlimit:4;stroke-opacity:0.96862745;stroke-dasharray:none"
 />
+        <path
+           transform="matrix(0.23144871,0,0,0.23144871,286.76018,350.69905)"
+           d="m 400,396.11218 c 0,10.35534 -8.39466,18.75 -18.75,18.75 -10.35534,0 -18.75,-8.39466 
-18.75,-18.75 0,-10.35534 8.39466,-18.75 18.75,-18.75 10.35534,0 18.75,8.39466 18.75,18.75 z"
+           sodipodi:ry="18.75"
+           sodipodi:rx="18.75"
+           sodipodi:cy="396.11218"
+           sodipodi:cx="381.25"
+           id="path3783"
+           
style="fill:#ffffff;fill-opacity:1;stroke:#5e9cf1;stroke-width:4.32061148;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:0.96862745;stroke-dasharray:none;stroke-dashoffset:0.7"
+           sodipodi:type="arc" />
+        <path
+           sodipodi:type="arc"
+           
style="fill:#ffffff;fill-opacity:1;stroke:#0c438d;stroke-width:6.2157526;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:0.96862745;stroke-dasharray:none;stroke-dashoffset:0.7"
+           id="path3779"
+           sodipodi:cx="381.25"
+           sodipodi:cy="396.11218"
+           sodipodi:rx="18.75"
+           sodipodi:ry="18.75"
+           d="m 400,396.11218 c 0,10.35534 -8.39466,18.75 -18.75,18.75 -10.35534,0 -18.75,-8.39466 
-18.75,-18.75 0,-10.35534 8.39466,-18.75 18.75,-18.75 10.35534,0 18.75,8.39466 18.75,18.75 z"
+           transform="matrix(0.17705667,0,0,0.17705667,307.49715,372.2444)" />
+      </g>
+    </g>
+  </g>
+</svg>
diff --git a/data/icons/hicolor/scalable/actions/gps-marker.svg 
b/data/icons/hicolor/scalable/actions/gps-marker.svg
new file mode 100644
index 00000000..564a38a4
--- /dev/null
+++ b/data/icons/hicolor/scalable/actions/gps-marker.svg
@@ -0,0 +1,370 @@
+<?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.48.3.1 r9886"
+   sodipodi:docname="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" />
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="4"
+     inkscape:cx="-23.119863"
+     inkscape:cy="10.735606"
+     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="844"
+     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="g3902"
+       transform="translate(0,75.224057)">
+      <g
+         id="g3035">
+        <path
+           transform="matrix(1.3594635,0,0,1,-187.87288,1)"
+           d="m 382.5,464.11218 c 0,1.51879 -3.69365,2.75 -8.25,2.75 -4.55635,0 -8.25,-1.23121 -8.25,-2.75 
0,-1.51878 3.69365,-2.75 8.25,-2.75 4.55635,0 8.25,1.23122 8.25,2.75 z"
+           sodipodi:ry="2.75"
+           sodipodi:rx="8.25"
+           sodipodi:cy="464.11218"
+           sodipodi:cx="374.25"
+           id="path3873"
+           
style="opacity:0.58662612;fill:url(#radialGradient3945);fill-opacity:1;stroke:none;filter:url(#filter3803)"
+           sodipodi:type="arc" />
+        <path
+           sodipodi:nodetypes="sszczss"
+           inkscape:connector-curvature="0"
+           id="path3875"
+           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(#linearGradient3947);fill-opacity:1;stroke:#982f26;stroke-width:1;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
 />
+        <path
+           
style="fill:none;stroke:#e19089;stroke-width:1.03512061;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+           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="path3877"
+           inkscape:connector-curvature="0"
+           sodipodi:nodetypes="sszczss" />
+        <path
+           sodipodi:type="arc"
+           
style="fill:#ffffff;fill-opacity:1;stroke:#e19089;stroke-width:4.32061148;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0.7"
+           id="path3879"
+           sodipodi:cx="381.25"
+           sodipodi:cy="396.11218"
+           sodipodi:rx="18.75"
+           sodipodi:ry="18.75"
+           d="m 400,396.11218 c 0,10.35534 -8.39466,18.75 -18.75,18.75 -10.35534,0 -18.75,-8.39466 
-18.75,-18.75 0,-10.35534 8.39466,-18.75 18.75,-18.75 10.35534,0 18.75,8.39466 18.75,18.75 z"
+           transform="matrix(0.23144871,0,0,0.23144871,232.66651,350.69905)" />
+        <path
+           transform="matrix(0.17705667,0,0,0.17705667,253.40348,372.2444)"
+           d="m 400,396.11218 c 0,10.35534 -8.39466,18.75 -18.75,18.75 -10.35534,0 -18.75,-8.39466 
-18.75,-18.75 0,-10.35534 8.39466,-18.75 18.75,-18.75 10.35534,0 18.75,8.39466 18.75,18.75 z"
+           sodipodi:ry="18.75"
+           sodipodi:rx="18.75"
+           sodipodi:cy="396.11218"
+           sodipodi:cx="381.25"
+           id="path3881"
+           
style="fill:#ffffff;fill-opacity:1;stroke:#982f26;stroke-width:6.2157526;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0.7"
+           sodipodi:type="arc" />
+      </g>
+    </g>
+  </g>
+</svg>
diff --git a/src/MapWidget.vala b/src/MapWidget.vala
new file mode 100644
index 00000000..87777e36
--- /dev/null
+++ b/src/MapWidget.vala
@@ -0,0 +1,397 @@
+/* Copyright 2016 Software Freedom Conservancy Inc.
+ *
+ * This software is licensed under the GNU LGPL (version 2.1 or later).
+ * See the COPYING file in this distribution.
+ */
+
+private class PositionMarker : Object {
+    private MapWidget map_widget;
+
+    protected PositionMarker.from_group(MapWidget map_widget) {
+        this.map_widget = map_widget;
+    }
+
+    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)
+                return true;
+            map_widget.select_data_view(this);
+            return true;
+        });
+        marker.enter_event.connect ((event) => {
+            map_widget.highlight_data_view(this);
+            return true;
+        });
+        marker.leave_event.connect ((event) => {
+            map_widget.unhighlight_data_view(this);
+            return true;
+        });
+        this.marker = marker;
+    }
+
+    public bool selected {
+        get {
+            return marker.get_selected();
+        }
+        set {
+            marker.set_selected(value);
+            if (!(marker is Champlain.Point)) {
+                // first child of the marker is a ClutterGroup which contains the texture
+                var t = (Clutter.Texture) marker.get_first_child().get_first_child();
+                if (value) {
+                    t.set_cogl_texture(map_widget.marker_selected_cogl_texture);
+                } else {
+                    t.set_cogl_texture(map_widget.marker_cogl_texture);
+                }
+            }
+        }
+    }
+
+    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; }
+}
+
+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;
+    }
+    public void add_marker(PositionMarker marker) {
+        markers.add(marker);
+    }
+    public Gee.Set<PositionMarker> get_markers() {
+        return markers;
+    }
+}
+
+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();
+    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;
+
+    public Cogl.Handle marker_cogl_texture { get; private set; }
+    public Cogl.Handle marker_selected_cogl_texture { get; private set; }
+
+    private MapWidget() {
+        add(gtk_champlain_widget);
+        setup_map();
+    }
+
+    public static MapWidget get_instance() {
+        if (instance == null)
+            instance = new MapWidget();
+        return instance;
+    }
+
+    public override void drag_data_received(Gdk.DragContext context, int x, int y,
+        Gtk.SelectionData selection_data, uint info, uint time) {
+        bool success = false;
+        Gee.List<MediaSource>? media = unserialize_media_sources(selection_data.get_data(),
+            selection_data.get_length());
+        if (media != null && media.size > 0) {
+            double lat = map_view.y_to_latitude(y);
+            double lon = map_view.x_to_longitude(x);
+            success = internal_drop_received(media, lat, lon);
+        }
+
+        Gtk.drag_finish(context, success, false, time);
+    }
+
+    public void set_page(Page page) {
+        this.page = page;
+    }
+
+    public void clear() {
+        marker_layer.remove_all();
+        marker_groups_tree.clear();
+        marker_groups.clear();
+        position_markers.clear();
+    }
+
+    public void add_position_marker(DataView view) {
+        DataSource view_source = view.get_source();
+        if (!(view_source is Positionable)) {
+            return;
+        }
+        Positionable p = (Positionable) view_source;
+        GpsCoords gps_coords = p.get_gps_coords();
+        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);
+    }
+
+    public void show_position_markers() {
+        if (!position_markers.is_empty) {
+            if (map_view.get_zoom_level() < DEFAULT_ZOOM_LEVEL) {
+                map_view.set_zoom_level(DEFAULT_ZOOM_LEVEL);
+            }
+            Champlain.BoundingBox bbox = marker_layer.get_bounding_box();
+            map_view.ensure_visible(bbox, true);
+        }
+    }
+
+    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) {
+            Marker marked = page_view.start_marking();
+            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))) {
+
+                // 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);
+            }
+            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 highlight_position_marker(DataView v) {
+        PositionMarker? m = position_markers.get(v);
+        if (m != null) {
+            m.selected = true;
+        }
+    }
+
+    public void unhighlight_position_marker(DataView v) {
+        PositionMarker? m = position_markers.get(v);
+        if (m != null) {
+            m.selected = false;
+        }
+    }
+
+    private void setup_map() {
+        map_view = gtk_champlain_widget.get_view();
+        map_view.add_layer(marker_layer);
+
+        // add scale to bottom left corner of the map
+        map_scale.content_gravity = Clutter.ContentGravity.BOTTOM_LEFT;
+        map_scale.connect_view(map_view);
+        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);
+
+        Gtk.TargetEntry[] dnd_targets = {
+            LibraryWindow.DND_TARGET_ENTRIES[LibraryWindow.TargetType.URI_LIST],
+            LibraryWindow.DND_TARGET_ENTRIES[LibraryWindow.TargetType.MEDIA_LIST]
+        };
+        Gtk.drag_dest_set(this, Gtk.DestDefaults.ALL, dnd_targets,
+            Gdk.DragAction.COPY | Gdk.DragAction.LINK | Gdk.DragAction.ASK);
+        button_press_event.connect(map_zoom_handler);
+        set_size_request(200, 200);
+
+        // Load gdk pixbuf via Resources class
+        Gdk.Pixbuf gdk_marker = Resources.get_icon(Resources.ICON_GPS_MARKER);
+        Gdk.Pixbuf gdk_marker_selected = Resources.get_icon(Resources.ICON_GPS_MARKER_SELECTED);
+        try {
+            // this is what GtkClutter.Texture.set_from_pixmap does
+            var tex = new Clutter.Texture(); // TODO: DEPRECATED Use Clutter.Image
+            tex.set_from_rgb_data(gdk_marker.get_pixels(),
+                                            gdk_marker.get_has_alpha(),
+                                            gdk_marker.get_width(),
+                                            gdk_marker.get_height(),
+                                            gdk_marker.get_rowstride(),
+                                            gdk_marker.get_has_alpha() ? 4 : 3,
+                                            Clutter.TextureFlags.NONE);
+            marker_cogl_texture = tex.get_cogl_texture();
+            tex.set_from_rgb_data(gdk_marker_selected.get_pixels(),
+                                            gdk_marker_selected.get_has_alpha(),
+                                            gdk_marker_selected.get_width(),
+                                            gdk_marker_selected.get_height(),
+                                            gdk_marker_selected.get_rowstride(),
+                                            gdk_marker_selected.get_has_alpha() ? 4 : 3,
+                                            Clutter.TextureFlags.NONE);
+            marker_selected_cogl_texture = tex.get_cogl_texture();
+        } catch (GLib.Error e) {
+            // Fall back to the generic champlain marker
+            marker_cogl_texture = null;
+            marker_selected_cogl_texture = null;
+        }
+    }
+
+    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();
+        assert(gps_coords.has_gps > 0);
+        Champlain.Marker champlain_marker;
+        if (marker_cogl_texture == null) {
+            // Fall back to the generic champlain marker
+            champlain_marker = new Champlain.Point.full(12, { red:10, green:10, blue:255, alpha:255 });
+        } else {
+            champlain_marker = new Champlain.Marker();
+            var t = new Clutter.Texture();
+            t.set_cogl_texture(marker_cogl_texture);
+            champlain_marker.add_child(t);
+        }
+        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);
+    }
+
+    private void add_marker(Champlain.Marker marker) {
+        marker_layer.add_marker(marker);
+    }
+
+    private bool map_zoom_handler(Gdk.EventButton event) {
+        if (event.type == Gdk.EventType.2BUTTON_PRESS) {
+            if (event.button == 1 || event.button == 3) {
+                double lat = map_view.y_to_latitude(event.y);
+                double lon = map_view.x_to_longitude(event.x);
+                if (event.button == 1) {
+                    map_view.zoom_in();
+                } else {
+                    map_view.zoom_out();
+                }
+                map_view.center_on(lat, lon);
+                return true;
+            }
+        }
+        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) {
+        int i = 0;
+        bool success = false;
+        while (i < media.size) {
+            Positionable p = media.get(i) as Positionable;
+            if (p != null) {
+                GpsCoords gps_coords = GpsCoords() {
+                    has_gps = 1,
+                    latitude = lat,
+                    longitude = lon
+                };
+                p.set_gps_coords(gps_coords);
+                success = true;
+            }
+            ++i;
+        }
+        return success;
+    }
+}
diff --git a/src/MetadataWriter.vala b/src/MetadataWriter.vala
index 0c232605..f55a11eb 100644
--- a/src/MetadataWriter.vala
+++ b/src/MetadataWriter.vala
@@ -15,7 +15,7 @@ public class MetadataWriter : Object {
     public const uint COMMIT_DELAY_MSEC = 3000;
     public const uint COMMIT_SPACING_MSEC = 50;
     
-    private const string[] INTERESTED_PHOTO_METADATA_DETAILS = { "name", "comment", "rating", 
"exposure-time" };
+    private const string[] INTERESTED_PHOTO_METADATA_DETAILS = { "name", "comment", "rating", 
"exposure-time", "gps" };
     
     private class CommitJob : BackgroundJob {
         public LibraryPhoto photo;
@@ -120,6 +120,14 @@ public class MetadataWriter : Object {
                 changed = true;
             }
 
+            // gps location
+            GpsCoords current_gps_coords = photo.get_gps_coords();
+            GpsCoords metadata_gps_coords = metadata.get_gps_coords();
+            if (!current_gps_coords.equals(ref metadata_gps_coords)) {
+                metadata.set_gps_coords(current_gps_coords);
+                changed = true;
+            }
+
             // tags (keywords) ... replace (or clear) entirely rather than union or intersection
             Gee.Set<string> safe_keywords = new Gee.HashSet<string>();
 
diff --git a/src/Page.vala b/src/Page.vala
index 93cedd56..666dbdf1 100644
--- a/src/Page.vala
+++ b/src/Page.vala
@@ -1242,6 +1242,7 @@ public abstract class CheckerboardPage : Page {
     private bool autoscroll_scheduled = false;
     private CheckerboardItem activated_item = null;
     private Gee.ArrayList<CheckerboardItem> previously_selected = null;
+    private MapWidget map_widget = null;
 
     public enum Activator {
         KEYBOARD,
@@ -1298,6 +1299,8 @@ public abstract class CheckerboardPage : Page {
         
         // scrollbar policy
         set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
+
+        map_widget = MapWidget.get_instance();
     }
     
     public void init_item_context_menu(string path) {
@@ -1751,6 +1754,7 @@ public abstract class CheckerboardPage : Page {
         // either something new is highlighted or now hovering over empty space, so dim old item
         if (current_hovered_item != null) {
             current_hovered_item.handle_mouse_leave();
+            map_widget.unhighlight_position_marker(current_hovered_item);
             current_hovered_item = null;
         }
         
@@ -1761,7 +1765,8 @@ public abstract class CheckerboardPage : Page {
         // brighten the new item
         current_hovered_item = item;
         current_hovered_item.handle_mouse_enter();
-        
+        map_widget.highlight_position_marker(item);
+
         return true;
     }
     
diff --git a/src/Photo.vala b/src/Photo.vala
index a858ae31..9135eea5 100644
--- a/src/Photo.vala
+++ b/src/Photo.vala
@@ -158,7 +158,7 @@ public enum Rating {
 // particular photo without modifying the backing image file.  The interface allows for
 // transformations to be stored persistently elsewhere or in memory until they're committed en
 // masse to an image file.
-public abstract class Photo : PhotoSource, Dateable {
+public abstract class Photo : PhotoSource, Dateable, Positionable {
     // Need to use "thumb" rather than "photo" for historical reasons -- this name is used
     // directly to load thumbnails from disk by already-existing filenames
     public const string TYPENAME = "thumb";
@@ -1222,6 +1222,7 @@ public abstract class Photo : PhotoSource, Dateable {
         Orientation orientation = Orientation.TOP_LEFT;
         time_t exposure_time = 0;
         string title = "";
+        GpsCoords gps_coords = GpsCoords();
         string comment = "";
         Rating rating = Rating.UNRATED;
         
@@ -1237,6 +1238,7 @@ public abstract class Photo : PhotoSource, Dateable {
             
             orientation = detected.metadata.get_orientation();
             title = detected.metadata.get_title();
+            gps_coords = detected.metadata.get_gps_coords();
             comment = detected.metadata.get_comment();
             params.keywords = detected.metadata.get_keywords();
             rating = detected.metadata.get_rating();
@@ -1272,6 +1274,7 @@ public abstract class Photo : PhotoSource, Dateable {
         params.row.flags = 0;
         params.row.master.file_format = detected.file_format;
         params.row.title = title;
+        params.row.gps_coords = gps_coords;
         params.row.comment = comment;
         params.row.rating = rating;
         
@@ -1313,6 +1316,7 @@ public abstract class Photo : PhotoSource, Dateable {
         params.row.flags = 0;
         params.row.master.file_format = PhotoFileFormat.JFIF;
         params.row.title = null;
+        params.row.gps_coords = GpsCoords();
         params.row.comment = null;
         params.row.rating = Rating.UNRATED;
         
@@ -1465,7 +1469,9 @@ public abstract class Photo : PhotoSource, Dateable {
             list += "image:orientation";
             updated_row.master.original_orientation = backing.original_orientation;
         }
-        
+
+        GpsCoords gps_coords = GpsCoords();
+
         if (detected.metadata != null) {
             MetadataDateTime? date_time = detected.metadata.get_exposure_date_time();
             if (date_time != null && updated_row.exposure_time != date_time.get_timestamp())
@@ -1473,6 +1479,11 @@ public abstract class Photo : PhotoSource, Dateable {
             
             if (updated_row.title != detected.metadata.get_title())
                 list += "metadata:name";
+
+            gps_coords = detected.metadata.get_gps_coords();
+            if (updated_row.gps_coords != gps_coords)
+                list += "metadata:gps";
+
             
             if (updated_row.comment != detected.metadata.get_comment())
                 list += "metadata:comment";
@@ -1493,7 +1504,8 @@ public abstract class Photo : PhotoSource, Dateable {
             MetadataDateTime? date_time = detected.metadata.get_exposure_date_time();
             if (date_time != null)
                 updated_row.exposure_time = date_time.get_timestamp();
-            
+
+            updated_row.gps_coords = gps_coords;
             updated_row.title = detected.metadata.get_title();
             updated_row.comment = detected.metadata.get_comment();
             updated_row.rating = detected.metadata.get_rating();
@@ -1604,6 +1616,7 @@ public abstract class Photo : PhotoSource, Dateable {
         
         if (reimport_state.metadata != null) {
             set_title(reimport_state.metadata.get_title());
+            set_gps_coords(reimport_state.metadata.get_gps_coords());
             set_comment(reimport_state.metadata.get_comment());
             set_rating(reimport_state.metadata.get_rating());
             apply_user_metadata_for_reimport(reimport_state.metadata);
@@ -2365,6 +2378,29 @@ public abstract class Photo : PhotoSource, Dateable {
         if (committed)
             notify_altered(new Alteration("metadata", "name"));
     }
+
+    public GpsCoords get_gps_coords() {
+        lock (row) {
+            return row.gps_coords;
+        }
+    }
+
+    public void set_gps_coords(GpsCoords gps_coords) {
+        DatabaseError dberr = null;
+        lock (row) {
+            try {
+                PhotoTable.get_instance().set_gps_coords(row.photo_id, gps_coords);
+                row.gps_coords = gps_coords;
+            } catch (DatabaseError err) {
+                dberr = err;
+            }
+        }
+        if (dberr == null)
+            notify_altered(new Alteration("metadata", "gps"));
+        else
+            warning("Unable to write gps coordinates for %s: %s", to_string(), dberr.message);
+    }
+
     
     public override bool set_comment(string? comment) {
         string? new_comment = prep_comment(comment);
@@ -3215,6 +3251,7 @@ public abstract class Photo : PhotoSource, Dateable {
         double orientation_time = 0.0;
         
         total_timer.start();
+
 #endif
         
         // get required fields all at once, to avoid holding the row lock
@@ -4979,7 +5016,12 @@ public class LibraryPhoto : Photo, Flaggable, Monitorable {
         this.import_keywords = null;
         
         thumbnail_scheduler = new OneShotScheduler("LibraryPhoto", generate_thumbnails);
-        
+        // import gps coords of photos imported with prior versions of shotwell
+        if (row.gps_coords.has_gps == -1) {
+            var gps_import_scheduler = new OneShotScheduler("LibraryPhoto", import_gps_metadata);
+            gps_import_scheduler.at_priority_idle(Priority.LOW);
+        }
+
         // if marked in a state where they're held in an orphanage, rehydrate their backlinks
         if ((row.flags & (FLAG_TRASH | FLAG_OFFLINE)) != 0)
             rehydrate_backlinks(global, row.backlinks);
@@ -5100,7 +5142,12 @@ public class LibraryPhoto : Photo, Flaggable, Monitorable {
         // fire signal that thumbnails have changed
         notify_thumbnail_altered();
     }
-    
+
+    private void import_gps_metadata() {
+        GpsCoords gps_coords = get_metadata().get_gps_coords();
+        set_gps_coords(gps_coords);
+    }
+
     // These keywords are only used during import and should not be relied upon elsewhere.
     public Gee.Collection<string>? get_import_keywords() {
         return import_keywords;
diff --git a/src/Properties.vala b/src/Properties.vala
index 2a07bb00..c7a094a2 100644
--- a/src/Properties.vala
+++ b/src/Properties.vala
@@ -4,12 +4,16 @@
  * See the COPYING file in this distribution.
  */
 
-private abstract class Properties : Gtk.Grid {
-    uint line_count = 0;
+private abstract class Properties : Gtk.Box {
+    protected Gtk.Grid grid = new Gtk.Grid();
+    protected uint line_count = 0;
 
     public Properties() {
-        row_spacing = 6;
-        column_spacing = 12;
+        grid.row_spacing = 6;
+        grid.column_spacing = 12;
+        set_homogeneous(false);
+        set_orientation(Gtk.Orientation.VERTICAL);
+        pack_start(grid, false, false, 0);
     }
 
     protected void add_line(string label_text, string info_text, bool multi_line = false, string? href = 
null) {
@@ -62,12 +66,12 @@ private abstract class Properties : Gtk.Grid {
             info = (Gtk.Widget) info_label;
         }
 
-        attach(label, 0, (int) line_count, 1, 1);
+        grid.attach(label, 0, (int) line_count, 1, 1);
 
         if (multi_line) {
-            attach(info, 1, (int) line_count, 1, 3);
+            grid.attach(info, 1, (int) line_count, 1, 3);
         } else {
-            attach(info, 1, (int) line_count, 1, 1);
+            grid.attach(info, 1, (int) line_count, 1, 1);
         }
 
         line_count++;
@@ -140,9 +144,9 @@ private abstract class Properties : Gtk.Grid {
     }
 
     protected virtual void clear_properties() {
-        foreach (Gtk.Widget child in get_children())
-            remove(child);
-        
+        foreach (Gtk.Widget child in grid.get_children())
+            grid.remove(child);
+
         line_count = 0;
     }
 
@@ -171,8 +175,11 @@ private class BasicProperties : Properties {
     private double clip_duration;
     private string raw_developer;
     private string raw_assoc;
+    private MapWidget map_widget;
 
     public BasicProperties() {
+        map_widget = MapWidget.get_instance();
+        pack_start(map_widget, true, true, 0);
     }
 
     protected override void clear_properties() {
@@ -190,6 +197,7 @@ private class BasicProperties : Properties {
         clip_duration = 0.0;
         raw_developer = "";
         raw_assoc = "";
+        map_widget.clear();
     }
 
     protected override void get_single_properties(DataView view) {
@@ -260,6 +268,8 @@ private class BasicProperties : Properties {
             }
             end_time = start_time;
         }
+        map_widget.add_position_marker(view);
+
     }
 
     protected override void get_multiple_properties(Gee.Iterable<DataView>? iter) {
@@ -269,8 +279,8 @@ private class BasicProperties : Properties {
         video_count = 0;
         foreach (DataView view in iter) {
             DataSource source = view.get_source();
-            
-            if (source is PhotoSource || source is PhotoImportSource) {                  
+
+            if (source is PhotoSource || source is PhotoImportSource) {
                 time_t exposure_time = (source is PhotoSource) ?
                     ((PhotoSource) source).get_exposure_time() :
                     ((PhotoImportSource) source).get_exposure_time();
@@ -282,7 +292,7 @@ private class BasicProperties : Properties {
                     if (end_time == 0 || exposure_time > end_time)
                         end_time = exposure_time;
                 }
-                
+
                 photo_count++;
             } else if (source is EventSource) {
                 EventSource event_source = (EventSource) source;
@@ -324,12 +334,14 @@ private class BasicProperties : Properties {
 
                 video_count++;
             }
+            map_widget.add_position_marker(view);
         }
     }
 
     protected override void get_properties(Page current_page) {
         base.get_properties(current_page);
 
+        map_widget.set_page(current_page);
         if (end_time == 0)
             end_time = start_time;
         if (start_time == 0)
@@ -455,6 +467,8 @@ private class BasicProperties : Properties {
                 }
             }
         }
+
+        map_widget.show_position_markers();
     }
 }
 
@@ -485,7 +499,7 @@ private class ExtendedProperties : Properties {
 
     public ExtendedProperties() {
         base();
-        row_spacing = 6;
+        grid.row_spacing = 6;
     }
 
     // Event stuff
diff --git a/src/Resources.vala b/src/Resources.vala
index 689b4da3..54c109c6 100644
--- a/src/Resources.vala
+++ b/src/Resources.vala
@@ -82,6 +82,8 @@ along with Shotwell; if not, write to the Free Software Foundation, Inc.,
     public const int ICON_FILTER_REJECTED_OR_BETTER_FIXED_SIZE = 32;
     public const int ICON_FILTER_UNRATED_OR_BETTER_FIXED_SIZE = 16;
     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_CAMERAS = "camera-photo-symbolic";
     public const string ICON_EVENTS = "multiple-events-symbolic";
diff --git a/src/core/SourceInterfaces.vala b/src/core/SourceInterfaces.vala
index 91a8acad..6e0c149a 100644
--- a/src/core/SourceInterfaces.vala
+++ b/src/core/SourceInterfaces.vala
@@ -42,3 +42,18 @@ public interface Indexable : DataSource {
     }
 }
 
+// Positionable DataSources provide a globally locatable point in longitude and latitude degrees
+
+public struct GpsCoords {
+    public int has_gps;
+    public double latitude;
+    public double longitude;
+    public bool equals(ref GpsCoords gps) {
+        return (has_gps == 0 && gps.has_gps == 0) || (latitude == gps.latitude && longitude == 
gps.longitude);
+    }
+}
+
+public interface Positionable : DataSource {
+    public abstract GpsCoords get_gps_coords();
+    public abstract void set_gps_coords(GpsCoords gps_coords);
+}
diff --git a/src/db/DatabaseTable.vala b/src/db/DatabaseTable.vala
index 64ef9cd7..dc69f2c5 100644
--- a/src/db/DatabaseTable.vala
+++ b/src/db/DatabaseTable.vala
@@ -21,6 +21,7 @@ public abstract class DatabaseTable {
      * tables are created on demand and tables and columns are easily ignored when already present.
      * However, the change should be noted in upgrade_database() as a comment.
      ***/
+    public const int SCHEMA_VERSION = 21;
 #if ENABLE_FACES
     public const int SCHEMA_VERSION = 21;
 #else
@@ -291,7 +292,19 @@ public abstract class DatabaseTable {
         if (res != Sqlite.DONE)
             throw_error("DatabaseTable.update_int64_by_id_2 %s.%s".printf(table_name, column), res);
     }
-    
+
+    protected void update_double_by_id_2(int64 id, string column, double value) throws DatabaseError {
+        Sqlite.Statement stmt;
+        prepare_update_by_id(id, column, out stmt);
+
+        int res = stmt.bind_double(1, value);
+        assert(res == Sqlite.OK);
+
+        res = stmt.step();
+        if (res != Sqlite.DONE)
+            throw_error("DatabaseTable.update_double_by_id_2 %s.%s".printf(table_name, column), res);
+    }
+
     protected void delete_by_id(int64 id) throws DatabaseError {
         Sqlite.Statement stmt;
         int res = db.prepare_v2("DELETE FROM %s WHERE id=?".printf(table_name), -1, out stmt);
diff --git a/src/db/Db.vala b/src/db/Db.vala
index ac24f113..34646324 100644
--- a/src/db/Db.vala
+++ b/src/db/Db.vala
@@ -293,7 +293,7 @@ private VerifyResult upgrade_database(int input_version) {
     }
     
     version = 16;
-    
+
     //
     // Version 17:
     // * Added comment column to PhotoTable and VideoTable
@@ -376,10 +376,24 @@ private VerifyResult upgrade_database(int input_version) {
     version = 21;
 #endif
     
+    //
+    // Version 21:
+    // * Add has_gps, gps_lat and gps_lon columns to PhotoTable
+
+    if (!DatabaseTable.ensure_column("PhotoTable", "has_gps", "INTEGER DEFAULT -1",
+        "upgrade_database: adding gps_lat column to PhotoTable")
+        || !DatabaseTable.ensure_column("PhotoTable", "gps_lat", "REAL",
+        "upgrade_database: adding gps_lat column to PhotoTable")
+        || !DatabaseTable.ensure_column("PhotoTable", "gps_lon", "REAL",
+        "upgrade_database: adding gps_lon column to PhotoTable")) {
+        return VerifyResult.UPGRADE_ERROR;
+    }
+
+    version = 21;
     //
     // Finalize the upgrade process
     //
-    
+
     assert(version == DatabaseTable.SCHEMA_VERSION);
     VersionTable.get_instance().update_version(version, Resources.APP_VERSION);
     
diff --git a/src/db/PhotoTable.vala b/src/db/PhotoTable.vala
index 24cec863..369c0a31 100644
--- a/src/db/PhotoTable.vala
+++ b/src/db/PhotoTable.vala
@@ -84,6 +84,7 @@ public class PhotoRow {
     public uint64 flags;
     public Rating rating;
     public string title;
+    public GpsCoords gps_coords;
     public string comment;
     public string? backlinks;
     public time_t time_reimported;
@@ -103,6 +104,10 @@ public class PhotoRow {
         development_ids = new BackingPhotoID[RawDeveloper.as_array().length];
         foreach (RawDeveloper d in RawDeveloper.as_array())
             development_ids[d] = BackingPhotoID();
+        gps_coords = GpsCoords();
+        development_ids = new BackingPhotoID[RawDeveloper.as_array().length];
+        foreach (RawDeveloper d in RawDeveloper.as_array())
+            development_ids[d] = BackingPhotoID();
     }
 }
 
@@ -140,6 +145,9 @@ public class PhotoTable : DatabaseTable {
             + "develop_shotwell_id INTEGER DEFAULT -1, "
             + "develop_camera_id INTEGER DEFAULT -1, "
             + "develop_embedded_id INTEGER DEFAULT -1, "
+            + "has_gps INTEGER DEFAULT -1, "
+            + "gps_lat REAL, "
+            + "gps_lon REAL, "
             + "comment TEXT"
             + ")", -1, out stmt);
         assert(res == Sqlite.OK);
@@ -209,8 +217,8 @@ public class PhotoTable : DatabaseTable {
         int res = db.prepare_v2(
             "INSERT INTO PhotoTable (filename, width, height, filesize, timestamp, exposure_time, "
             + "orientation, original_orientation, import_id, event_id, md5, thumbnail_md5, "
-            + "exif_md5, time_created, file_format, title, rating, editable_id, developer, comment) "
-            + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+            + "exif_md5, time_created, file_format, title, rating, editable_id, developer, has_gps, gps_lat, 
gps_lon, comment) "
+            + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
             -1, out stmt);
         assert(res == Sqlite.OK);
         
@@ -254,7 +262,13 @@ public class PhotoTable : DatabaseTable {
         assert(res == Sqlite.OK);
         res = stmt.bind_text(19, photo_row.developer.to_string());
         assert(res == Sqlite.OK);
-        res = stmt.bind_text(20, photo_row.comment);
+        res = stmt.bind_int(20, photo_row.gps_coords.has_gps);
+        assert(res == Sqlite.OK);
+        res = stmt.bind_double(21, photo_row.gps_coords.latitude);
+        assert(res == Sqlite.OK);
+        res = stmt.bind_double(22, photo_row.gps_coords.longitude);
+        assert(res == Sqlite.OK);
+        res = stmt.bind_text(23, photo_row.comment);
         assert(res == Sqlite.OK);
         
         res = stmt.step();
@@ -285,7 +299,8 @@ public class PhotoTable : DatabaseTable {
         int res = db.prepare_v2(
             "UPDATE PhotoTable SET width = ?, height = ?, filesize = ?, timestamp = ?, "
             + "exposure_time = ?, orientation = ?, original_orientation = ?, md5 = ?, " 
-            + "exif_md5 = ?, thumbnail_md5 = ?, file_format = ?, title = ?, time_reimported = ? "
+            + "exif_md5 = ?, thumbnail_md5 = ?, file_format = ?, title = ?, "
+            + "has_gps = ?, gps_lat = ?, gps_lon = ?, time_reimported = ? "
             + "WHERE id = ?", -1, out stmt);
         assert(res == Sqlite.OK);
         
@@ -315,9 +330,15 @@ public class PhotoTable : DatabaseTable {
         assert(res == Sqlite.OK);
         res = stmt.bind_text(12, row.title);
         assert(res == Sqlite.OK);
-        res = stmt.bind_int64(13, time_reimported);
+        res = stmt.bind_int(13, row.gps_coords.has_gps);
+        assert(res == Sqlite.OK);
+        res = stmt.bind_double(14, row.gps_coords.latitude);
+        assert(res == Sqlite.OK);
+        res = stmt.bind_double(15, row.gps_coords.longitude);
+        assert(res == Sqlite.OK);
+        res = stmt.bind_int64(16, time_reimported);
         assert(res == Sqlite.OK);
-        res = stmt.bind_int64(14, row.photo_id.id);
+        res = stmt.bind_int64(17, row.photo_id.id);
         assert(res == Sqlite.OK);
         
         res = stmt.step();
@@ -390,7 +411,7 @@ public class PhotoTable : DatabaseTable {
             + "original_orientation, import_id, event_id, transformations, md5, thumbnail_md5, "
             + "exif_md5, time_created, flags, rating, file_format, title, backlinks, "
             + "time_reimported, editable_id, metadata_dirty, developer, develop_shotwell_id, "
-            + "develop_camera_id, develop_embedded_id, comment "
+            + "develop_camera_id, develop_embedded_id, has_gps, gps_lat, gps_lon, comment "
             + "FROM PhotoTable WHERE id=?", 
             -1, out stmt);
         assert(res == Sqlite.OK);
@@ -430,7 +451,10 @@ public class PhotoTable : DatabaseTable {
         row.development_ids[RawDeveloper.SHOTWELL] = BackingPhotoID(stmt.column_int64(24));
         row.development_ids[RawDeveloper.CAMERA] = BackingPhotoID(stmt.column_int64(25));
         row.development_ids[RawDeveloper.EMBEDDED] = BackingPhotoID(stmt.column_int64(26));
-        row.comment = stmt.column_text(27);
+        row.gps_coords.has_gps = stmt.column_int(27);
+        row.gps_coords.latitude = stmt.column_double(28);
+        row.gps_coords.longitude = stmt.column_double(29);
+        row.comment = stmt.column_text(30);
         
         return row;
     }
@@ -442,7 +466,7 @@ public class PhotoTable : DatabaseTable {
             + "original_orientation, import_id, event_id, transformations, md5, thumbnail_md5, "
             + "exif_md5, time_created, flags, rating, file_format, title, backlinks, time_reimported, "
             + "editable_id, metadata_dirty, developer, develop_shotwell_id, develop_camera_id, " 
-            + "develop_embedded_id, comment FROM PhotoTable", 
+            + "develop_embedded_id, has_gps, gps_lat, gps_lon, comment FROM PhotoTable",
             -1, out stmt);
         assert(res == Sqlite.OK);
         
@@ -478,7 +502,10 @@ public class PhotoTable : DatabaseTable {
             row.development_ids[RawDeveloper.SHOTWELL] = BackingPhotoID(stmt.column_int64(25));
             row.development_ids[RawDeveloper.CAMERA] = BackingPhotoID(stmt.column_int64(26));
             row.development_ids[RawDeveloper.EMBEDDED] = BackingPhotoID(stmt.column_int64(27));
-            row.comment = stmt.column_text(28);
+            row.gps_coords.has_gps = stmt.column_int(28);
+            row.gps_coords.latitude = stmt.column_double(29);
+            row.gps_coords.longitude = stmt.column_double(30);
+            row.comment = stmt.column_text(31);
             
             validate_orientation(row);
             
@@ -500,9 +527,9 @@ public class PhotoTable : DatabaseTable {
         int res = db.prepare_v2("INSERT INTO PhotoTable (filename, width, height, filesize, "
             + "timestamp, exposure_time, orientation, original_orientation, import_id, event_id, "
             + "transformations, md5, thumbnail_md5, exif_md5, time_created, flags, rating, "
-            + "file_format, title, editable_id, developer, develop_shotwell_id, develop_camera_id, "
-            + "develop_embedded_id, comment) "
-            + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+            + "file_format, title, has_gps, gps_lat, gps_lon, editable_id, developer, "
+            + "develop_shotwell_id, develop_camera_id, develop_embedded_id, comment) "
+            + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
             -1, out stmt);
         assert(res == Sqlite.OK);
         
@@ -544,18 +571,23 @@ public class PhotoTable : DatabaseTable {
         assert(res == Sqlite.OK);
         res = stmt.bind_text(19, original.title);
         assert(res == Sqlite.OK);
-        res = stmt.bind_int64(20, editable_id.id);
+        res = stmt.bind_int(20, original.gps_coords.has_gps);
         assert(res == Sqlite.OK);
-        
-        res = stmt.bind_text(21, original.developer.to_string());
+        res = stmt.bind_double(21, original.gps_coords.latitude);
+        assert(res == Sqlite.OK);
+        res = stmt.bind_double(22, original.gps_coords.longitude);
+        assert(res == Sqlite.OK);
+        res = stmt.bind_int64(23, editable_id.id);
         assert(res == Sqlite.OK);
-        res = stmt.bind_int64(22, develop_shotwell.id);
+        res = stmt.bind_text(24, original.developer.to_string());
         assert(res == Sqlite.OK);
-        res = stmt.bind_int64(23, develop_camera_id.id);
+        res = stmt.bind_int64(25, develop_shotwell.id);
         assert(res == Sqlite.OK);
-        res = stmt.bind_int64(24, develop_embedded_id.id);
+        res = stmt.bind_int64(26, develop_camera_id.id);
         assert(res == Sqlite.OK);
-        res = stmt.bind_text(25, original.comment);
+        res = stmt.bind_int64(27, develop_embedded_id.id);
+        assert(res == Sqlite.OK);
+        res = stmt.bind_text(28, original.comment);
         assert(res == Sqlite.OK);
         
         res = stmt.step();
@@ -572,7 +604,15 @@ public class PhotoTable : DatabaseTable {
     public bool set_title(PhotoID photo_id, string? new_title) {
        return update_text_by_id(photo_id.id, "title", new_title != null ? new_title : "");
     }
-    
+
+    public void set_gps_coords(PhotoID photo_id, GpsCoords new_gps_coords) throws DatabaseError {
+        update_int_by_id_2(photo_id.id, "has_gps", new_gps_coords.has_gps);
+        if (new_gps_coords.has_gps > 0) {
+            update_double_by_id_2(photo_id.id, "gps_lat", new_gps_coords.latitude);
+            update_double_by_id_2(photo_id.id, "gps_lon", new_gps_coords.longitude);
+        }
+    }
+
     public bool set_comment(PhotoID photo_id, string? new_comment) {
        return update_text_by_id(photo_id.id, "comment", new_comment != null ? new_comment : "");
     }
diff --git a/src/library/LibraryWindow.vala b/src/library/LibraryWindow.vala
index 3300c6c3..079e801d 100644
--- a/src/library/LibraryWindow.vala
+++ b/src/library/LibraryWindow.vala
@@ -144,7 +144,9 @@ public class LibraryWindow : AppWindow {
     private Gtk.ProgressBar background_progress_bar = new Gtk.ProgressBar();
     private bool background_progress_displayed = false;
     
-    private BasicProperties basic_properties = new BasicProperties();
+    // Instantiate later in constructor becase the map support loads its icons in there and we need
+    // to have the global app instance available for that
+    private BasicProperties basic_properties;
     private ExtendedProperties extended_properties = new ExtendedProperties();
     private Gtk.Revealer extended_properties_revealer = new Gtk.Revealer();
     
@@ -192,6 +194,7 @@ public class LibraryWindow : AppWindow {
         search_toolbar = new SearchFilterToolbar(search_actions);
 
         // create the main layout & start at the Library page
+        basic_properties = new BasicProperties();
         create_layout(library_branch.photos_entry.get_page());
         
         // settings that should persist between sessions
@@ -1168,11 +1171,11 @@ public class LibraryWindow : AppWindow {
         basic_properties.halign = Gtk.Align.FILL;
         basic_properties.valign = Gtk.Align.CENTER;
         basic_properties.hexpand = true;
-        basic_properties.vexpand = false;
+        basic_properties.vexpand = true;
         basic_properties.margin_top = 10;
         basic_properties.margin_bottom = 10;
         basic_properties.margin_start = 6;
-        basic_properties.margin_end = 0;
+        basic_properties.margin_end = 6;
 
         bottom_frame.add(basic_properties);
         bottom_frame.get_style_context().remove_class("frame");
diff --git a/src/main.vala b/src/main.vala
index a7339ed8..4add1df6 100644
--- a/src/main.vala
+++ b/src/main.vala
@@ -370,7 +370,7 @@ void main(string[] args) {
     
     // init GTK (valac has already called g_threads_init())
     try {
-        Gtk.init_with_args(ref args, _("[FILE]"), CommandlineOptions.entries,
+        GtkClutter.init_with_args(ref args, _("[FILE]"), CommandlineOptions.entries,
             Resources.APP_GETTEXT_PACKAGE);
 
         var use_dark = Config.Facade.get_instance().get_gtk_theme_variant();
diff --git a/src/photos/PhotoMetadata.vala b/src/photos/PhotoMetadata.vala
index 74dc5591..cd890c56 100644
--- a/src/photos/PhotoMetadata.vala
+++ b/src/photos/PhotoMetadata.vala
@@ -1168,7 +1168,27 @@ public class PhotoMetadata : MediaMetadata {
         
         return true;
     }
-    
+
+    public GpsCoords get_gps_coords() {
+        GpsCoords gps_coords = GpsCoords();
+        double altitude;
+        gps_coords.has_gps = exiv2.get_gps_info(out gps_coords.longitude, out gps_coords.latitude, out 
altitude) ? 1 : 0;
+        if (gps_coords.has_gps > 0) {
+            if (get_string("Exif.GPSInfo.GPSLongitudeRef") == "W" && gps_coords.longitude > 0)
+                gps_coords.longitude = -gps_coords.longitude;
+            if (get_string("Exif.GPSInfo.GPSLatitudeRef") == "S" && gps_coords.latitude > 0)
+                gps_coords.latitude = -gps_coords.latitude;
+        }
+        return gps_coords;
+    }
+
+    public void set_gps_coords(GpsCoords gps_coords) {
+        if (gps_coords.has_gps > 0)
+            exiv2.set_gps_info(gps_coords.longitude, gps_coords.latitude, 0.0);
+        else
+            exiv2.delete_gps_info();
+    }
+
     public bool get_exposure(out MetadataRational exposure) {
         return get_rational("Exif.Photo.ExposureTime", out exposure);
     }
diff --git a/src/util/misc.vala b/src/util/misc.vala
index 6111ea3f..d1d6431a 100644
--- a/src/util/misc.vala
+++ b/src/util/misc.vala
@@ -273,7 +273,9 @@ public class OneShotScheduler {
     }
     
     public void at_idle() {
-        at_priority_idle(Priority.DEFAULT_IDLE);
+        // needs to be lower (higher priority) than Clutter.PRIORITY_REDRAW which is
+        // set at Priority.HIGH_IDLE + 50
+        at_priority_idle(Priority.HIGH_IDLE + 40);
     }
     
     public void at_priority_idle(int priority) {


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