From f2f79117991c04f254ab86de16788990153ed5ac Mon Sep 17 00:00:00 2001 From: grey Date: Sat, 1 Apr 2023 18:44:31 +0200 Subject: [PATCH] inital commit --- .gitignore | 4 + README.MD | 10 + data/points.json | 380 ++++ gdal2tiles.py | 2925 +++++++++++++++++++++++++++ index.html | 181 ++ rastercoords.js | 90 + static/leaflet-editable-polyline.js | 589 ++++++ tools/createtiles.sh | 17 + watermark.js | 14 + 9 files changed, 4210 insertions(+) create mode 100644 .gitignore create mode 100644 README.MD create mode 100644 data/points.json create mode 100644 gdal2tiles.py create mode 100644 index.html create mode 100644 rastercoords.js create mode 100644 static/leaflet-editable-polyline.js create mode 100755 tools/createtiles.sh create mode 100644 watermark.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..18b0de8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +20230330_143536-cropped.jpg +asci.txt +20230330_143536.jpg +tiles/ \ No newline at end of file diff --git a/README.MD b/README.MD new file mode 100644 index 0000000..16c691f --- /dev/null +++ b/README.MD @@ -0,0 +1,10 @@ +# Whats on my Laptop? +A little application to show others what the stickers on your laptop mean. +Build using only static HTML, CSS and JavaScript. Utilizes Leaflet.js for showing the device. + +## How to use +1. Take a picture of your laptop with the stickers on it. +2. Run tools/createtiles.sh (you have to edit the script to fit your needs) +3. (optional) Delete orginal picture +4. Update image resoluton in index.html +5. (optional) Update metadata in index.html \ No newline at end of file diff --git a/data/points.json b/data/points.json new file mode 100644 index 0000000..5b23378 --- /dev/null +++ b/data/points.json @@ -0,0 +1,380 @@ +{ + "aoi": [ + { + "points": [ + { + "lat": -37.7255859375, + "lng": 155.9580078125 + }, + { + "lat": -37.25, + "lng": 166 + }, + { + "lat": -36.859375, + "lng": 166.41015625 + }, + { + "lat": -35.2578125, + "lng": 166.36328125 + }, + { + "lat": -35.2109375, + "lng": 166.4921875 + }, + { + "lat": -37.12109375, + "lng": 167.87109375 + }, + { + "lat": -37.2109375, + "lng": 168.578125 + }, + { + "lat": -35.52734375, + "lng": 174.34765625 + }, + { + "lat": -34.875, + "lng": 175.0625 + }, + { + "lat": -34.3203125, + "lng": 175.15234375 + }, + { + "lat": -33.7734375, + "lng": 175 + }, + { + "lat": -32.78125, + "lng": 174.50390625 + }, + { + "lat": -26.296875, + "lng": 171.1484375 + }, + { + "lat": -25.97265625, + "lng": 170.69140625 + }, + { + "lat": -25.9765625, + "lng": 169.86328125 + }, + { + "lat": -26.35546875, + "lng": 169.359375 + }, + { + "lat": -27, + "lng": 168.875 + }, + { + "lat": -27.7734375, + "lng": 168.4375 + }, + { + "lat": -27.3984375, + "lng": 167.89453125 + }, + { + "lat": -27.25, + "lng": 167.3125 + }, + { + "lat": -27.2890625, + "lng": 166.77734375 + }, + { + "lat": -27.4375, + "lng": 166.5 + }, + { + "lat": -27.62890625, + "lng": 166.0234375 + }, + { + "lat": -27.453125, + "lng": 165.74609375 + }, + { + "lat": -26.9609375, + "lng": 165.7578125 + }, + { + "lat": -26.6875, + "lng": 165.625 + }, + { + "lat": -26.37890625, + "lng": 165.22265625 + }, + { + "lat": -26.42578125, + "lng": 164.0234375 + }, + { + "lat": -26.798828125, + "lng": 155.57421875 + }, + { + "lat": -26.810546875, + "lng": 155.27734375 + }, + { + "lat": -26.970703125, + "lng": 155.00390625 + }, + { + "lat": -27.322265625, + "lng": 154.791015625 + }, + { + "lat": -37.4375, + "lng": 155.4375 + }, + { + "lat": -37.7236328125, + "lng": 155.9541015625 + } + ], + "title": "TypeScript Loves Prisma", + "description": "TypeScript Loves Prisma, a love which shall never end!" + }, + { + "points": [ + { + "lat": -58.462890625, + "lng": 165.0546875 + }, + { + "lat": -58.359375, + "lng": 165.609375 + }, + { + "lat": -58.34765625, + "lng": 166.109375 + }, + { + "lat": -58.4375, + "lng": 166.6171875 + }, + { + "lat": -59.51171875, + "lng": 167.63671875 + }, + { + "lat": -61.5390625, + "lng": 169.73828125 + }, + { + "lat": -61.73046875, + "lng": 169.89453125 + }, + { + "lat": -61.7578125, + "lng": 170.107421875 + }, + { + "lat": -61.421875, + "lng": 171.0390625 + }, + { + "lat": -61.3984375, + "lng": 172.328125 + }, + { + "lat": -61.548828125, + "lng": 173.14453125 + }, + { + "lat": -61.71875, + "lng": 173.890625 + }, + { + "lat": -62.7109375, + "lng": 175.1953125 + }, + { + "lat": -63.609375, + "lng": 175.7890625 + }, + { + "lat": -64.4609375, + "lng": 176.1015625 + }, + { + "lat": -65.25, + "lng": 176.375 + }, + { + "lat": -66.1953125, + "lng": 176.3515625 + }, + { + "lat": -67.3046875, + "lng": 176.0859375 + }, + { + "lat": -67.6796875, + "lng": 175.859375 + }, + { + "lat": -69.9375, + "lng": 178.0234375 + }, + { + "lat": -70.6875, + "lng": 178.7109375 + }, + { + "lat": -71.953125, + "lng": 178.79296875 + }, + { + "lat": -72.5, + "lng": 178.484375 + }, + { + "lat": -79.328125, + "lng": 169.234375 + }, + { + "lat": -79.5546875, + "lng": 168.82421875 + }, + { + "lat": -79.55078125, + "lng": 168.33203125 + }, + { + "lat": -79.375, + "lng": 167.453125 + }, + { + "lat": -78.765625, + "lng": 166.5 + }, + { + "lat": -74.484375, + "lng": 161.41015625 + }, + { + "lat": -73.5, + "lng": 161.07421875 + }, + { + "lat": -69.625, + "lng": 161.908203125 + }, + { + "lat": -68.69921875, + "lng": 161.04296875 + }, + { + "lat": -67.59375, + "lng": 160.5859375 + }, + { + "lat": -66.47265625, + "lng": 160.5390625 + }, + { + "lat": -65.345703125, + "lng": 160.8671875 + }, + { + "lat": -64.234375, + "lng": 161.671875 + }, + { + "lat": -63.72265625, + "lng": 162.412109375 + }, + { + "lat": -63.404296875, + "lng": 163.193359375 + }, + { + "lat": -60.42578125, + "lng": 163.828125 + }, + { + "lat": -59.2109375, + "lng": 164.1875 + }, + { + "lat": -58.462890625, + "lng": 165.0478515625 + } + ], + "title": "Funny Prisma", + "description": "Look! Its the Prisma Logo with googly eyes! :D" + }, + { + "points": [{"lat":-68.7734375,"lng":136.966796875},{"lat":-68.5859375,"lng":137.216796875},{"lat":-68.453125,"lng":137.53125},{"lat":-68.529296875,"lng":137.986328125},{"lat":-68.6953125,"lng":138.3984375},{"lat":-68.796875,"lng":139.859375},{"lat":-68.6953125,"lng":141.484375},{"lat":-68.375,"lng":143.0703125},{"lat":-66.9296875,"lng":146.875},{"lat":-66.046875,"lng":148.453125},{"lat":-65.109375,"lng":149.7421875},{"lat":-63.140625,"lng":151.75},{"lat":-62.1640625,"lng":152.5},{"lat":-61.0859375,"lng":153.0078125},{"lat":-59.2890625,"lng":153.84375},{"lat":-58.375,"lng":154.1953125},{"lat":-57.390625,"lng":154.40625},{"lat":-56.6015625,"lng":154.65625},{"lat":-54.34375,"lng":154.8359375},{"lat":-53.1875,"lng":154.7265625},{"lat":-52.75,"lng":154.4921875},{"lat":-52.34375,"lng":154.2734375},{"lat":-51.7421875,"lng":153.6796875},{"lat":-50.65625,"lng":153.6796875},{"lat":-50.265625,"lng":153.8203125},{"lat":-49.5234375,"lng":154.953125},{"lat":-49.234375,"lng":156.4609375},{"lat":-49.6328125,"lng":157.296875},{"lat":-50.1015625,"lng":157.75},{"lat":-51.734375,"lng":158.2265625},{"lat":-52.9375,"lng":158.6875},{"lat":-53.75,"lng":159.046875},{"lat":-54.359375,"lng":159.640625},{"lat":-55.90625,"lng":160.171875},{"lat":-57.78125,"lng":160.9375},{"lat":-60.078125,"lng":161.2265625},{"lat":-62.4375,"lng":161.46875},{"lat":-66.3125,"lng":160.65625},{"lat":-69.9609375,"lng":158.796875},{"lat":-72.7265625,"lng":156.40625},{"lat":-74.6328125,"lng":153.5234375},{"lat":-75.828125,"lng":150.4453125},{"lat":-76.40625,"lng":147.203125},{"lat":-76.2734375,"lng":144.75},{"lat":-75.78125,"lng":142.4921875},{"lat":-74.1875,"lng":138.5546875},{"lat":-72.3203125,"lng":135.984375},{"lat":-71.609375,"lng":135.625},{"lat":-69.8203125,"lng":136.484375},{"lat":-68.765625,"lng":136.96875}], + "title": "Bannana", + "description": "It's a bannana for scale! Thanks Linus!" + }, { + "points": [{"lat":-86.765625,"lng":134.34375},{"lat":-86.5546875,"lng":144.29296875},{"lat":-87.39453125,"lng":145.01953125},{"lat":-87.203125,"lng":147.5234375},{"lat":-87.0625,"lng":149.46875},{"lat":-86.80078125,"lng":152.3359375},{"lat":-85.51953125,"lng":152.796875},{"lat":-84.5625,"lng":165.65625},{"lat":-84.8046875,"lng":166.2734375},{"lat":-85.71875,"lng":166.40625},{"lat":-85.6484375,"lng":168.7265625},{"lat":-81.7109375,"lng":169.0625},{"lat":-80.7578125,"lng":169.625},{"lat":-80.1328125,"lng":170.53125},{"lat":-80.0078125,"lng":174.171875},{"lat":-80.6171875,"lng":175.3046875},{"lat":-82.4375,"lng":176.0390625},{"lat":-84.609375,"lng":176.0390625},{"lat":-88.8125,"lng":176.53125},{"lat":-93,"lng":176.8125},{"lat":-94.515625,"lng":176.9453125},{"lat":-95.7890625,"lng":176.8046875},{"lat":-96.25,"lng":170.375},{"lat":-94.0703125,"lng":169.8828125},{"lat":-92.1484375,"lng":169.7265625},{"lat":-91.75,"lng":169.390625},{"lat":-91.859375,"lng":168.75},{"lat":-93.0546875,"lng":167.484375},{"lat":-92.6484375,"lng":164.546875},{"lat":-91.796875,"lng":163.6484375},{"lat":-92.1171875,"lng":157.8046875},{"lat":-92.078125,"lng":155.9765625},{"lat":-93.46875,"lng":154.7890625},{"lat":-93.7890625,"lng":152.46875},{"lat":-93.390625,"lng":151.2734375},{"lat":-92.8125,"lng":150.7734375},{"lat":-92.90625,"lng":147.7734375},{"lat":-92.953125,"lng":140.90625},{"lat":-93.75,"lng":135.0703125},{"lat":-93.4609375,"lng":134.3046875},{"lat":-86.78125,"lng":134.25}], + "title": "Blackmagicdesign", + "description": "Makers of the best video editing software in the world!" + }, + { + "points": [{"lat":-53.8671875,"lng":139.578125},{"lat":-54.0703125,"lng":139.9375},{"lat":-53.9140625,"lng":140.8671875},{"lat":-53.4375,"lng":141.5234375},{"lat":-42.8359375,"lng":149.1484375},{"lat":-42.3125,"lng":149.3359375},{"lat":-41.796875,"lng":149.296875},{"lat":-41.296875,"lng":149.125},{"lat":-40.6875,"lng":148.546875},{"lat":-38.4609375,"lng":145.3984375},{"lat":-37.9375,"lng":145.0859375},{"lat":-35.7578125,"lng":142.2734375},{"lat":-35.1328125,"lng":141.296875},{"lat":-34.453125,"lng":141.1328125},{"lat":-32.9921875,"lng":139.609375},{"lat":-32.1875,"lng":138.25},{"lat":-31.8671875,"lng":137.5},{"lat":-30.984375,"lng":136.609375},{"lat":-30.578125,"lng":135.953125},{"lat":-30.4921875,"lng":135.3828125},{"lat":-29.5,"lng":134.25},{"lat":-28.4296875,"lng":132.5625},{"lat":-26.671875,"lng":130.265625},{"lat":-26.283203125,"lng":129.3515625},{"lat":-26.4375,"lng":128.328125},{"lat":-25.9296875,"lng":127.60546875},{"lat":-25.5,"lng":126.69140625},{"lat":-25.5703125,"lng":125.3671875},{"lat":-26.1015625,"lng":124.2109375},{"lat":-26.87109375,"lng":123.505859375},{"lat":-27.734375,"lng":123.09375},{"lat":-30.046875,"lng":121.1484375},{"lat":-31.5625,"lng":119.8203125},{"lat":-32.1875,"lng":119.59375},{"lat":-33.05078125,"lng":119.701171875},{"lat":-33.6640625,"lng":119.234375},{"lat":-36.859375,"lng":116.8125},{"lat":-37.6328125,"lng":116.484375},{"lat":-38.4453125,"lng":116.546875},{"lat":-39.234375,"lng":117.1328125},{"lat":-39.515625,"lng":117.6640625},{"lat":-39.474609375,"lng":118.611328125},{"lat":-39.310546875,"lng":118.947265625},{"lat":-39.734375,"lng":119.3359375},{"lat":-40.5390625,"lng":120.234375},{"lat":-41.359375,"lng":121.2890625},{"lat":-42.8203125,"lng":123.6640625},{"lat":-42.984375,"lng":124.390625},{"lat":-42.8359375,"lng":125.046875},{"lat":-43.90625,"lng":126.4765625},{"lat":-44.6015625,"lng":126.6171875},{"lat":-45.109375,"lng":126.90625},{"lat":-47.9375,"lng":130.6875},{"lat":-50.6875,"lng":134.5390625},{"lat":-50.7890625,"lng":135.1796875},{"lat":-53.75,"lng":139.2734375}], + "title": "Computer Components", + "description": "Some assorted computer components." + }, + { + "points": [{"lat":-50.6875,"lng":128},{"lat":-51.296875,"lng":130.328125},{"lat":-52.46875,"lng":132.65625},{"lat":-54.109375,"lng":135.21875},{"lat":-56.015625,"lng":137.328125},{"lat":-57.046875,"lng":137.78125},{"lat":-60.171875,"lng":138.25},{"lat":-60.53515625,"lng":138.14453125},{"lat":-60.84375,"lng":138.3671875},{"lat":-61.41796875,"lng":138.453125},{"lat":-62.28515625,"lng":138.37109375},{"lat":-62.9453125,"lng":138.02734375},{"lat":-63.84765625,"lng":137.9296875},{"lat":-64.671875,"lng":137.7578125},{"lat":-66.63671875,"lng":137.28515625},{"lat":-67.55859375,"lng":136.87890625},{"lat":-67.83203125,"lng":136.3515625},{"lat":-67.87890625,"lng":135.71484375},{"lat":-68.1015625,"lng":134.453125},{"lat":-68.359375,"lng":133.63671875},{"lat":-68.734375,"lng":133.5703125},{"lat":-69.52734375,"lng":133.6953125},{"lat":-70.33984375,"lng":133.87890625},{"lat":-70.84765625,"lng":133.8671875},{"lat":-71.09375,"lng":133.8359375},{"lat":-71.2734375,"lng":133.64453125},{"lat":-71.35546875,"lng":133.2421875},{"lat":-71.25,"lng":132.9375},{"lat":-70.921875,"lng":132.640625},{"lat":-68.84765625,"lng":132.5546875},{"lat":-69.3828125,"lng":130.140625},{"lat":-69.55078125,"lng":128.72265625},{"lat":-69.125,"lng":127.5625},{"lat":-65.90625,"lng":123.78125},{"lat":-64.08203125,"lng":122.3203125},{"lat":-62.125,"lng":121.5625},{"lat":-61.6171875,"lng":121.28125},{"lat":-60.921875,"lng":121.28125},{"lat":-59.75,"lng":121.375},{"lat":-57.359375,"lng":121.796875},{"lat":-52.75,"lng":123.671875},{"lat":-51.3359375,"lng":125.0078125},{"lat":-50.8359375,"lng":125.703125},{"lat":-50.6484375,"lng":126.6875},{"lat":-50.6796875,"lng":127.921875}], + "title": "Nanoleaf", + "description": "Yes, I know I made a mistake while applying the sticker. Yes it also bothers me." + }, + { + "points": [{"lat":-39.9609375,"lng":116.388671875},{"lat":-39.763671875,"lng":117.234375},{"lat":-39.82421875,"lng":118.0390625},{"lat":-40.244140625,"lng":118.734375},{"lat":-40.828125,"lng":119.2265625},{"lat":-56.3125,"lng":120.9375},{"lat":-57.4375,"lng":120.8046875},{"lat":-58.2734375,"lng":120.1953125},{"lat":-58.7734375,"lng":118.9609375},{"lat":-58.71875,"lng":117.40625},{"lat":-58.2578125,"lng":108.5859375},{"lat":-58.12890625,"lng":107.66015625},{"lat":-57.71875,"lng":106.9453125},{"lat":-57.125,"lng":106.4140625},{"lat":-56.234375,"lng":106.23828125},{"lat":-49.984375,"lng":104.82421875},{"lat":-49.12109375,"lng":105.01953125},{"lat":-48.62109375,"lng":105.30078125},{"lat":-39.98828125,"lng":116.32421875}], + "title": "Holographic Prisma Logo", + "description": "A really cool looking holographic Prisma Logo sticker." + }, + { + "points": [{"lat":-58.859375,"lng":110.09375},{"lat":-59.3125,"lng":111.953125},{"lat":-60.56640625,"lng":113.72265625},{"lat":-61.515625,"lng":114.640625},{"lat":-62.59375,"lng":115.21875},{"lat":-65.046875,"lng":115.90625},{"lat":-66.953125,"lng":115.65625},{"lat":-68.75,"lng":114.9375},{"lat":-70.140625,"lng":113.90625},{"lat":-71.203125,"lng":112.453125},{"lat":-71.75,"lng":110.9375},{"lat":-71.984375,"lng":109.09375},{"lat":-71.8125,"lng":107.46875},{"lat":-71.265625,"lng":106.109375},{"lat":-70.3125,"lng":104.703125},{"lat":-69.140625,"lng":103.65625},{"lat":-66.375,"lng":102.6875},{"lat":-64.390625,"lng":102.640625},{"lat":-62.453125,"lng":103.296875},{"lat":-60.453125,"lng":104.984375},{"lat":-59.1875,"lng":107.125},{"lat":-58.875,"lng":109.765625}], + "title": "Digital Ocean Logo", + "description": "A Digital Ocean logo sticker. I won this from the Hacktoberfest!" + }, + { + "points": [{"lat":-46.4609375,"lng":88.3203125},{"lat":-49.46875,"lng":93.21875},{"lat":-52.734375,"lng":98.1953125},{"lat":-53.671875,"lng":98.8515625},{"lat":-54.78125,"lng":99.0625},{"lat":-66.7109375,"lng":96.3515625},{"lat":-67.96875,"lng":94.9765625},{"lat":-68.2578125,"lng":93.8046875},{"lat":-68.0234375,"lng":92.359375},{"lat":-61.6328125,"lng":82.5078125},{"lat":-60.375,"lng":81.8359375},{"lat":-59.28125,"lng":81.8125},{"lat":-47.96875,"lng":84.65625},{"lat":-46.6796875,"lng":85.9609375},{"lat":-46.3671875,"lng":87.1328125},{"lat":-46.4765625,"lng":88.1171875}], + "title": "Hacktoberfest No. 8", + "description": "A Hacktoberfest sticker. I won this from the Hacktoberfest!" + }, + { + "points": [{"lat":-67.453125,"lng":87.59375},{"lat":-68.8125,"lng":89.203125},{"lat":-71.578125,"lng":92.5},{"lat":-66.671875,"lng":96.390625},{"lat":-66.15625,"lng":96.984375},{"lat":-66.109375,"lng":97.921875},{"lat":-70.3125,"lng":103.09375},{"lat":-71.078125,"lng":103.578125},{"lat":-71.796875,"lng":103.609375},{"lat":-77.0625,"lng":99.3125},{"lat":-81.125,"lng":104.4375},{"lat":-81.78125,"lng":104.765625},{"lat":-82.421875,"lng":104.578125},{"lat":-87.91796875,"lng":100.32421875},{"lat":-88.21875,"lng":99.515625},{"lat":-88.0625,"lng":98.71875},{"lat":-74.75,"lng":82.421875},{"lat":-73.921875,"lng":81.8125},{"lat":-73.015625,"lng":81.875},{"lat":-67.71875,"lng":86.078125},{"lat":-67.35546875,"lng":86.77734375},{"lat":-67.44921875,"lng":87.5078125}], + "title": "WASD", + "description": "A WASD sticker." + }, + { + "points": [{"lat":-35.982421875,"lng":90.90625},{"lat":-28.578125,"lng":108.375},{"lat":-28.828125,"lng":109.703125},{"lat":-29.578125,"lng":110.421875},{"lat":-32.8125,"lng":111.96875},{"lat":-33.4375,"lng":112.171875},{"lat":-34.125,"lng":111.96875},{"lat":-35.03125,"lng":112.953125},{"lat":-36.109375,"lng":113.40625},{"lat":-37.90625,"lng":112.6875},{"lat":-38.455078125,"lng":112.650390625},{"lat":-38.6396484375,"lng":112.87109375},{"lat":-39.546875,"lng":113.0390625},{"lat":-40.078125,"lng":113.0078125},{"lat":-40.6015625,"lng":112.8671875},{"lat":-41.6875,"lng":112.4375},{"lat":-48.78125,"lng":95.859375},{"lat":-48.6875,"lng":95.046875},{"lat":-48.1875,"lng":94.546875},{"lat":-37.765625,"lng":90.23046875},{"lat":-36.177734375,"lng":90.83984375}], + "title": "Right to Repair", + "description": "A Right to Repair sticker. The most important right there is in the new digital world." + }, + { + "points": [{"lat":-74.640625,"lng":63.65625},{"lat":-74.265625,"lng":64.546875},{"lat":-74.203125,"lng":65.859375},{"lat":-74.828125,"lng":67.5625},{"lat":-74.015625,"lng":69.390625},{"lat":-73.87890625,"lng":70.62109375},{"lat":-74.0625,"lng":71.78125},{"lat":-74.5234375,"lng":72.6484375},{"lat":-74.125,"lng":73.15625},{"lat":-73.59375,"lng":73.609375},{"lat":-73.4140625,"lng":74.3359375},{"lat":-73.40625,"lng":75.3359375},{"lat":-72.5234375,"lng":76.09375},{"lat":-72.171875,"lng":77.0859375},{"lat":-72.4375,"lng":78.3359375},{"lat":-73.1328125,"lng":79.09375},{"lat":-72.890625,"lng":80.1328125},{"lat":-73.1875,"lng":81.265625},{"lat":-73.53125,"lng":81.953125},{"lat":-75.2421875,"lng":82.9296875},{"lat":-77.5625,"lng":83.1015625},{"lat":-78.1015625,"lng":82.984375},{"lat":-78.6875,"lng":82.2890625},{"lat":-78.859375,"lng":81.578125},{"lat":-80.359375,"lng":81.515625},{"lat":-80.9609375,"lng":82.1171875},{"lat":-81.484375,"lng":83.109375},{"lat":-81.96875,"lng":83.3984375},{"lat":-83.44921875,"lng":83.38671875},{"lat":-84.15625,"lng":82.93359375},{"lat":-84.7421875,"lng":81.91796875},{"lat":-86.50390625,"lng":82.03515625},{"lat":-87.0625,"lng":83.015625},{"lat":-87.9296875,"lng":83.7578125},{"lat":-89.73828125,"lng":83.84375},{"lat":-91.4765625,"lng":83.6796875},{"lat":-92.09375,"lng":83.3359375},{"lat":-92.421875,"lng":82.3515625},{"lat":-92.3359375,"lng":81.1875},{"lat":-92.0546875,"lng":80.3515625},{"lat":-92.2734375,"lng":79.953125},{"lat":-92.9453125,"lng":79.25},{"lat":-93.1796875,"lng":78.296875},{"lat":-93.0859375,"lng":77.3515625},{"lat":-92.611328125,"lng":76.755859375},{"lat":-92.609375,"lng":75.529296875},{"lat":-91.98046875,"lng":74.634765625},{"lat":-92.248046875,"lng":74.375},{"lat":-92.59375,"lng":74.171875},{"lat":-93.0078125,"lng":73.5390625},{"lat":-93.234375,"lng":72.28125},{"lat":-93.125,"lng":70.982421875},{"lat":-92.689453125,"lng":69.869140625},{"lat":-93.3984375,"lng":68.5546875},{"lat":-93.71875,"lng":67.171875},{"lat":-93.2265625,"lng":65.640625},{"lat":-92.09375,"lng":64.296875},{"lat":-91.8359375,"lng":63.4609375},{"lat":-91.1328125,"lng":62.9609375},{"lat":-89.9765625,"lng":62.8359375},{"lat":-89.578125,"lng":62.9765625},{"lat":-88.46875,"lng":63.0078125},{"lat":-87.9453125,"lng":63.3671875},{"lat":-87.40625,"lng":63.4765625},{"lat":-86.6171875,"lng":63.9140625},{"lat":-86.1875,"lng":64.9609375},{"lat":-85.671875,"lng":65.359375},{"lat":-85.390625,"lng":65.0546875},{"lat":-85.078125,"lng":64.2734375},{"lat":-83.984375,"lng":63.8125},{"lat":-82.9453125,"lng":63.8515625},{"lat":-82.3359375,"lng":64.3046875},{"lat":-81.9140625,"lng":65.125},{"lat":-80.796875,"lng":65.09375},{"lat":-79.703125,"lng":64.8359375},{"lat":-79.4921875,"lng":64.53125},{"lat":-79.328125,"lng":63.7265625},{"lat":-78.4609375,"lng":63.265625},{"lat":-75.4375,"lng":63.1015625},{"lat":-74.6875,"lng":63.6171875}], + "title": "Hacktoberfest \"H\"", + "description": "A Hacktoberfest sticker. I won this from the Hacktoberfest! Yaay." + }, + { + "points": [{"lat":-41.875,"lng":59.375},{"lat":-39.9765625,"lng":59.484375},{"lat":-37.5859375,"lng":60.8046875},{"lat":-36.8828125,"lng":61.5234375},{"lat":-35.96875,"lng":63.96875},{"lat":-35.5625,"lng":65.0546875},{"lat":-35.328125,"lng":65.8125},{"lat":-35.390625,"lng":66.71875},{"lat":-34.640625,"lng":67.2578125},{"lat":-34.0703125,"lng":68.5},{"lat":-33.390625,"lng":70.359375},{"lat":-32.2421875,"lng":70.484375},{"lat":-31.453125,"lng":70.984375},{"lat":-30.5859375,"lng":72.03125},{"lat":-30.1484375,"lng":73.3203125},{"lat":-29.875,"lng":74.3828125},{"lat":-30.15625,"lng":75.515625},{"lat":-29.8359375,"lng":76.0234375},{"lat":-29.640625,"lng":76.7109375},{"lat":-29.703125,"lng":77.4921875},{"lat":-29.9375,"lng":78.2109375},{"lat":-28.4375,"lng":78.03125},{"lat":-27.0078125,"lng":78.6484375},{"lat":-26.078125,"lng":80},{"lat":-25.953125,"lng":81.25},{"lat":-25.4375,"lng":82.125},{"lat":-25.0078125,"lng":82.78125},{"lat":-24.90625,"lng":83.65625},{"lat":-25.09375,"lng":84.5859375},{"lat":-25.4375,"lng":85.375},{"lat":-26.1015625,"lng":86.0703125},{"lat":-27,"lng":86.375},{"lat":-27.859375,"lng":86.5234375},{"lat":-28.75,"lng":86.3125},{"lat":-28.9609375,"lng":86.109375},{"lat":-33.6875,"lng":85.28125},{"lat":-34.859375,"lng":85.4765625},{"lat":-35.875,"lng":85.2890625},{"lat":-36.875,"lng":84.6171875},{"lat":-37.5,"lng":83.6875},{"lat":-37.6875,"lng":82.21875},{"lat":-38.6484375,"lng":81.0703125},{"lat":-38.84375,"lng":79.78125},{"lat":-39.65625,"lng":78.8125},{"lat":-40.2890625,"lng":77.1015625},{"lat":-40.828125,"lng":75.40625},{"lat":-40.6640625,"lng":74.28125},{"lat":-41.3359375,"lng":74.03125},{"lat":-42.6484375,"lng":71.3984375},{"lat":-43.875,"lng":68.5625},{"lat":-45.1875,"lng":65.8125},{"lat":-45.546875,"lng":64.2890625},{"lat":-45.3359375,"lng":62.6640625},{"lat":-44.484375,"lng":60.734375},{"lat":-42,"lng":59.4375}], + "title": "curl.//", + "description": "The seemingly most important lib there is. Curl. Meters <-> Pixels <-> Tile + + WGS84 coordinates Spherical Mercator Pixels in pyramid Tiles in pyramid + lat/lon XY in metres XY pixels Z zoom XYZ from TMS + EPSG:4326 EPSG:900913 + .----. --------- -- TMS + / \ <-> | | <-> /----/ <-> Google + \ / | | /--------/ QuadTree + ----- --------- /------------/ + KML, public WebMapService Web Clients TileMapService + + What is the coordinate extent of Earth in EPSG:900913? + + [-20037508.342789244, -20037508.342789244, 20037508.342789244, 20037508.342789244] + Constant 20037508.342789244 comes from the circumference of the Earth in meters, + which is 40 thousand kilometers, the coordinate origin is in the middle of extent. + In fact you can calculate the constant as: 2 * math.pi * 6378137 / 2.0 + $ echo 180 85 | gdaltransform -s_srs EPSG:4326 -t_srs EPSG:900913 + Polar areas with abs(latitude) bigger then 85.05112878 are clipped off. + + What are zoom level constants (pixels/meter) for pyramid with EPSG:900913? + + whole region is on top of pyramid (zoom=0) covered by 256x256 pixels tile, + every lower zoom level resolution is always divided by two + initialResolution = 20037508.342789244 * 2 / 256 = 156543.03392804062 + + What is the difference between TMS and Google Maps/QuadTree tile name convention? + + The tile raster itself is the same (equal extent, projection, pixel size), + there is just different identification of the same raster tile. + Tiles in TMS are counted from [0,0] in the bottom-left corner, id is XYZ. + Google placed the origin [0,0] to the top-left corner, reference is XYZ. + Microsoft is referencing tiles by a QuadTree name, defined on the website: + http://msdn2.microsoft.com/en-us/library/bb259689.aspx + + The lat/lon coordinates are using WGS84 datum, yeh? + + Yes, all lat/lon we are mentioning should use WGS84 Geodetic Datum. + Well, the web clients like Google Maps are projecting those coordinates by + Spherical Mercator, so in fact lat/lon coordinates on sphere are treated as if + the were on the WGS84 ellipsoid. + + From MSDN documentation: + To simplify the calculations, we use the spherical form of projection, not + the ellipsoidal form. Since the projection is used only for map display, + and not for displaying numeric coordinates, we don't need the extra precision + of an ellipsoidal projection. The spherical projection causes approximately + 0.33 percent scale distortion in the Y direction, which is not visually noticable. + + How do I create a raster in EPSG:900913 and convert coordinates with PROJ.4? + + You can use standard GIS tools like gdalwarp, cs2cs or gdaltransform. + All of the tools supports -t_srs 'epsg:900913'. + + For other GIS programs check the exact definition of the projection: + More info at http://spatialreference.org/ref/user/google-projection/ + The same projection is degined as EPSG:3785. WKT definition is in the official + EPSG database. + + Proj4 Text: + +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 + +k=1.0 +units=m +nadgrids=@null +no_defs + + Human readable WKT format of EPGS:900913: + PROJCS["Google Maps Global Mercator", + GEOGCS["WGS 84", + DATUM["WGS_1984", + SPHEROID["WGS 84",6378137,298.257223563, + AUTHORITY["EPSG","7030"]], + AUTHORITY["EPSG","6326"]], + PRIMEM["Greenwich",0], + UNIT["degree",0.0174532925199433], + AUTHORITY["EPSG","4326"]], + PROJECTION["Mercator_1SP"], + PARAMETER["central_meridian",0], + PARAMETER["scale_factor",1], + PARAMETER["false_easting",0], + PARAMETER["false_northing",0], + UNIT["metre",1, + AUTHORITY["EPSG","9001"]]] + """ + + def __init__(self, tileSize=256): + '''Initialize the TMS Global Mercator pyramid''' + + self.tileSize = tileSize + self.initialResolution = 2 * math.pi * 6378137 / self.tileSize + + # 156543.03392804062 for tileSize 256 pixels + + self.originShift = 2 * math.pi * 6378137 / 2.0 + + # 20037508.342789244 + + def LatLonToMeters(self, lat, lon): + '''Converts given lat/lon in WGS84 Datum to XY in Spherical Mercator EPSG:900913''' + + mx = lon * self.originShift / 180.0 + my = math.log(math.tan((90 + lat) * math.pi / 360.0)) \ + / (math.pi / 180.0) + + my = my * self.originShift / 180.0 + return (mx, my) + + def MetersToLatLon(self, mx, my): + '''Converts XY point from Spherical Mercator EPSG:900913 to lat/lon in WGS84 Datum''' + + lon = mx / self.originShift * 180.0 + lat = my / self.originShift * 180.0 + + lat = 180 / math.pi * (2 * math.atan(math.exp(lat * math.pi + / 180.0)) - math.pi / 2.0) + return (lat, lon) + + def PixelsToMeters( + self, + px, + py, + zoom, + ): + '''Converts pixel coordinates in given zoom level of pyramid to EPSG:900913''' + + res = self.Resolution(zoom) + mx = px * res - self.originShift + my = py * res - self.originShift + return (mx, my) + + def MetersToPixels( + self, + mx, + my, + zoom, + ): + '''Converts EPSG:900913 to pyramid pixel coordinates in given zoom level''' + + res = self.Resolution(zoom) + px = (mx + self.originShift) / res + py = (my + self.originShift) / res + return (px, py) + + def PixelsToTile(self, px, py): + '''Returns a tile covering region in given pixel coordinates''' + + tx = int(math.ceil(px / float(self.tileSize)) - 1) + ty = int(math.ceil(py / float(self.tileSize)) - 1) + return (tx, ty) + + def PixelsToRaster( + self, + px, + py, + zoom, + ): + '''Move the origin of pixel coordinates to top-left corner''' + + mapSize = self.tileSize << zoom + return (px, mapSize - py) + + def MetersToTile( + self, + mx, + my, + zoom, + ): + '''Returns tile for given mercator coordinates''' + + (px, py) = self.MetersToPixels(mx, my, zoom) + return self.PixelsToTile(px, py) + + def TileBounds( + self, + tx, + ty, + zoom, + ): + '''Returns bounds of the given tile in EPSG:900913 coordinates''' + + (minx, miny) = self.PixelsToMeters(tx * self.tileSize, ty + * self.tileSize, zoom) + (maxx, maxy) = self.PixelsToMeters((tx + 1) * self.tileSize, + (ty + 1) * self.tileSize, zoom) + return (minx, miny, maxx, maxy) + + def TileLatLonBounds( + self, + tx, + ty, + zoom, + ): + '''Returns bounds of the given tile in latutude/longitude using WGS84 datum''' + + bounds = self.TileBounds(tx, ty, zoom) + (minLat, minLon) = self.MetersToLatLon(bounds[0], bounds[1]) + (maxLat, maxLon) = self.MetersToLatLon(bounds[2], bounds[3]) + + return (minLat, minLon, maxLat, maxLon) + + def Resolution(self, zoom): + '''Resolution (meters/pixel) for given zoom level (measured at Equator)''' + + # return (2 * math.pi * 6378137) / (self.tileSize * 2**zoom) + + return self.initialResolution / 2 ** zoom + + def ZoomForPixelSize(self, pixelSize): + '''Maximal scaledown zoom of the pyramid closest to the pixelSize.''' + + for i in range(MAXZOOMLEVEL): + if pixelSize > self.Resolution(i): + if i != 0: + return i - 1 + else: + return 0 # We don't want to scale up + + def GoogleTile( + self, + tx, + ty, + zoom, + ): + '''Converts TMS tile coordinates to Google Tile coordinates''' + + # coordinate origin is moved from bottom-left to top-left corner of the extent + + return (tx, 2 ** zoom - 1 - ty) + + def QuadTree( + self, + tx, + ty, + zoom, + ): + '''Converts TMS tile coordinates to Microsoft QuadTree''' + + quadKey = '' + ty = 2 ** zoom - 1 - ty + for i in range(zoom, 0, -1): + digit = 0 + mask = 1 << i - 1 + if tx & mask != 0: + digit += 1 + if ty & mask != 0: + digit += 2 + quadKey += str(digit) + + return quadKey + + +# --------------------- + +class GlobalGeodetic(object): + + """ + TMS Global Geodetic Profile + --------------------------- + + Functions necessary for generation of global tiles in Plate Carre projection, + EPSG:4326, "unprojected profile". + + Such tiles are compatible with Google Earth (as any other EPSG:4326 rasters) + and you can overlay the tiles on top of OpenLayers base map. + + Pixel and tile coordinates are in TMS notation (origin [0,0] in bottom-left). + + What coordinate conversions do we need for TMS Global Geodetic tiles? + + Global Geodetic tiles are using geodetic coordinates (latitude,longitude) + directly as planar coordinates XY (it is also called Unprojected or Plate + Carre). We need only scaling to pixel pyramid and cutting to tiles. + Pyramid has on top level two tiles, so it is not square but rectangle. + Area [-180,-90,180,90] is scaled to 512x256 pixels. + TMS has coordinate origin (for pixels and tiles) in bottom-left corner. + Rasters are in EPSG:4326 and therefore are compatible with Google Earth. + + LatLon <-> Pixels <-> Tiles + + WGS84 coordinates Pixels in pyramid Tiles in pyramid + lat/lon XY pixels Z zoom XYZ from TMS + EPSG:4326 + .----. ---- + / \ <-> /--------/ <-> TMS + \ / /--------------/ + ----- /--------------------/ + WMS, KML Web Clients, Google Earth TileMapService + """ + + def __init__(self, tmscompatible, tileSize=256): + self.tileSize = tileSize + if tmscompatible is not None: + + # Defaults the resolution factor to 0.703125 (2 tiles @ level 0) + # Adhers to OSGeo TMS spec http://wiki.osgeo.org/wiki/Tile_Map_Service_Specification#global-geodetic + + self.resFact = 180.0 / self.tileSize + else: + + # Defaults the resolution factor to 1.40625 (1 tile @ level 0) + # Adheres OpenLayers, MapProxy, etc default resolution for WMTS + + self.resFact = 360.0 / self.tileSize + + def LonLatToPixels( + self, + lon, + lat, + zoom, + ): + '''Converts lon/lat to pixel coordinates in given zoom of the EPSG:4326 pyramid''' + + res = self.resFact / 2 ** zoom + px = (180 + lon) / res + py = (90 + lat) / res + return (px, py) + + def PixelsToTile(self, px, py): + '''Returns coordinates of the tile covering region in pixel coordinates''' + + tx = int(math.ceil(px / float(self.tileSize)) - 1) + ty = int(math.ceil(py / float(self.tileSize)) - 1) + return (tx, ty) + + def LonLatToTile( + self, + lon, + lat, + zoom, + ): + '''Returns the tile for zoom which covers given lon/lat coordinates''' + + (px, py) = self.LonLatToPixels(lon, lat, zoom) + return self.PixelsToTile(px, py) + + def Resolution(self, zoom): + '''Resolution (arc/pixel) for given zoom level (measured at Equator)''' + + return self.resFact / 2 ** zoom + + # return 180 / float( 1 << (8+zoom) ) + + def ZoomForPixelSize(self, pixelSize): + '''Maximal scaledown zoom of the pyramid closest to the pixelSize.''' + + for i in range(MAXZOOMLEVEL): + if pixelSize > self.Resolution(i): + if i != 0: + return i - 1 + else: + return 0 # We don't want to scale up + + def TileBounds( + self, + tx, + ty, + zoom, + ): + '''Returns bounds of the given tile''' + + res = self.resFact / 2 ** zoom + return (tx * self.tileSize * res - 180, ty * self.tileSize + * res - 90, (tx + 1) * self.tileSize * res - 180, (ty + + 1) * self.tileSize * res - 90) + + def TileLatLonBounds( + self, + tx, + ty, + zoom, + ): + '''Returns bounds of the given tile in the SWNE form''' + + b = self.TileBounds(tx, ty, zoom) + return (b[1], b[0], b[3], b[2]) + + +# --------------------- +# TODO: Finish Zoomify implemtentation!!! + +class Zoomify(object): + + """ + Tiles compatible with the Zoomify viewer + ---------------------------------------- + """ + + def __init__( + self, + width, + height, + tilesize=256, + tileformat='jpg', + ): + """Initialization of the Zoomify tile tree""" + + self.tilesize = tilesize + self.tileformat = tileformat + imagesize = (width, height) + tiles = (math.ceil(width / tilesize), math.ceil(height + / tilesize)) + + # Size (in tiles) for each tier of pyramid. + + self.tierSizeInTiles = [] + self.tierSizeInTiles.push(tiles) + + # Image size in pixels for each pyramid tierself + + self.tierImageSize = [] + self.tierImageSize.append(imagesize) + + while imagesize[0] > tilesize or imageSize[1] > tilesize: + imagesize = (math.floor(imagesize[0] / 2), + math.floor(imagesize[1] / 2)) + tiles = (math.ceil(imagesize[0] / tilesize), + math.ceil(imagesize[1] / tilesize)) + self.tierSizeInTiles.append(tiles) + self.tierImageSize.append(imagesize) + + self.tierSizeInTiles.reverse() + self.tierImageSize.reverse() + + # Depth of the Zoomify pyramid, number of tiers (zoom levels) + + self.numberOfTiers = len(self.tierSizeInTiles) + + # Number of tiles up to the given tier of pyramid. + + self.tileCountUpToTier = [] + self.tileCountUpToTier[0] = 0 + for i in range(1, self.numberOfTiers + 1): + self.tileCountUpToTier.append(self.tierSizeInTiles[i + - 1][0] * self.tierSizeInTiles[i - 1][1] + + self.tileCountUpToTier[i - 1]) + + def tilefilename( + self, + x, + y, + z, + ): + """Returns filename for tile with given coordinates""" + + tileIndex = x + y * self.tierSizeInTiles[z][0] \ + + self.tileCountUpToTier[z] + return os.path.join('TileGroup%.0f' % math.floor(tileIndex + / 256), '%s-%s-%s.%s' % (z, x, y, + self.tileformat)) + + +# ============================================================================= +# ============================================================================= +# ============================================================================= + +class GDAL2Tiles(object): + + # ------------------------------------------------------------------------- + + def process(self): + """The main processing function, runs all the main steps of processing""" + + # Opening and preprocessing of the input file + + self.open_input() + + # Generation of main metadata files and HTML viewers + + self.generate_metadata() + + # Generation of the lowest tiles + + self.generate_base_tiles() + + # Generation of the overview tiles (higher in the pyramid) + + self.generate_overview_tiles() + + # ------------------------------------------------------------------------- + + def error(self, msg, details=''): + """Print an error message and stop the processing""" + + if details: + self.parser.error(msg + ''' + +''' + details) + else: + self.parser.error(msg) + + # ------------------------------------------------------------------------- + + def progressbar(self, complete=0.0): + """Print progressbar for float value 0..1""" + + gdal.TermProgress_nocb(complete) + + # ------------------------------------------------------------------------- + + def stop(self): + """Stop the rendering immediately""" + + self.stopped = True + + # ------------------------------------------------------------------------- + + def __init__(self, arguments): + """Constructor function - initialization""" + + self.stopped = False + self.input = None + self.output = None + + # Tile format + + self.tilesize = 256 + self.tiledriver = 'PNG' + self.tileext = 'png' + + # Should we read bigger window of the input raster and scale it down? + # Note: Modified leter by open_input() + # Not for 'near' resampling + # Not for Wavelet based drivers (JPEG2000, ECW, MrSID) + # Not for 'raster' profile + + self.scaledquery = True + + # How big should be query window be for scaling down + # Later on reset according the chosen resampling algorightm + + self.querysize = 4 * self.tilesize + + # Should we use Read on the input file for generating overview tiles? + # Note: Modified later by open_input() + # Otherwise the overview tiles are generated from existing underlying tiles + + self.overviewquery = False + + # RUN THE ARGUMENT PARSER: + + self.optparse_init() + (self.options, self.args) = \ + self.parser.parse_args(args=arguments) + if not self.args: + self.error('No input file specified') + + # POSTPROCESSING OF PARSED ARGUMENTS: + + # Workaround for old versions of GDAL + + try: + if self.options.verbose and self.options.resampling \ + == 'near' or gdal.TermProgress_nocb: + pass + except: + self.error('This version of GDAL is not supported. Please upgrade to 1.6+.' + ) + + # ,"You can try run crippled version of gdal2tiles with parameters: -v -r 'near'") + + # Is output directory the last argument? + + # Test output directory, if it doesn't exist + + if os.path.isdir(self.args[-1]) or len(self.args) > 1 \ + and not os.path.exists(self.args[-1]): + self.output = self.args[-1] + self.args = self.args[:-1] + + # More files on the input not directly supported yet + + if len(self.args) > 1: + self.error('Processing of several input files is not supported.' + , + """Please first use a tool like gdal_vrtmerge.py or gdal_merge.py on the files: +gdal_vrtmerge.py -o merged.vrt %s""" + % ' '.join(self.args)) + + # TODO: Call functions from gdal_vrtmerge.py directly + + self.input = self.args[0] + + # Default values for not given options + + if not self.output: + + # Directory with input filename without extension in actual directory + + self.output = \ + os.path.splitext(os.path.basename(self.input))[0] + + if not self.options.title: + self.options.title = os.path.basename(self.input) + + if self.options.url and not self.options.url.endswith('/'): + self.options.url += '/' + if self.options.url: + self.options.url += os.path.basename(self.output) + '/' + + # Supported options + + self.resampling = None + + if self.options.resampling == 'average': + try: + if gdal.RegenerateOverview: + pass + except: + self.error("'average' resampling algorithm is not available." + , + "Please use -r 'near' argument or upgrade to newer version of GDAL." + ) + elif self.options.resampling == 'antialias': + + try: + if numpy: + pass + except: + self.error("'antialias' resampling algorithm is not available." + , + 'Install PIL (Python Imaging Library) and numpy.' + ) + elif self.options.resampling == 'near': + + self.resampling = gdal.GRA_NearestNeighbour + self.querysize = self.tilesize + elif self.options.resampling == 'bilinear': + + self.resampling = gdal.GRA_Bilinear + self.querysize = self.tilesize * 2 + elif self.options.resampling == 'cubic': + + self.resampling = gdal.GRA_Cubic + elif self.options.resampling == 'cubicspline': + + self.resampling = gdal.GRA_CubicSpline + elif self.options.resampling == 'lanczos': + + self.resampling = gdal.GRA_Lanczos + + # User specified zoom levels + + self.tminz = None + self.tmaxz = None + if self.options.zoom: + minmax = self.options.zoom.split('-', 1) + minmax.extend(['']) + (min, max) = minmax[:2] + self.tminz = int(min) + if max: + self.tmaxz = int(max) + else: + self.tmaxz = int(min) + + # KML generation + + self.kml = self.options.kml + + # Output the results + + if self.options.verbose: + print('Options:', self.options) + print('Input:', self.input) + print('Output:', self.output) + print('Cache: %s MB' % (gdal.GetCacheMax() / 1024 / 1024)) + print('') + + # ------------------------------------------------------------------------- + + def optparse_init(self): + """Prepare the option parser for input (argv)""" + + from optparse import OptionParser, OptionGroup + usage = 'Usage: %prog [options] input_file(s) [output]' + p = OptionParser(usage, version='%prog ' + __version__) + p.add_option( + '-p', + '--profile', + dest='profile', + type='choice', + choices=profile_list, + help="Tile cutting profile (%s) - default 'mercator' (Google Maps compatible)" + % ','.join(profile_list), + ) + p.add_option( + '-r', + '--resampling', + dest='resampling', + type='choice', + choices=resampling_list, + help="Resampling method (%s) - default 'average'" + % ','.join(resampling_list), + ) + p.add_option('-s', '--s_srs', dest='s_srs', metavar='SRS', + help='The spatial reference system used for the source input data' + ) + p.add_option('-z', '--zoom', dest='zoom', + help="Zoom levels to render (format:'2-5' or '10')." + ) + p.add_option('-e', '--resume', dest='resume', + action='store_true', + help='Resume mode. Generate only missing files.') + p.add_option('-a', '--srcnodata', dest='srcnodata', + metavar='NODATA', + help='NODATA transparency value to assign to the input data' + ) + p.add_option('-d', '--tmscompatible', dest='tmscompatible', + action='store_true', + help='When using the geodetic profile, specifies the base resolution as 0.703125 or 2 tiles at zoom level 0.' + ) + p.add_option('-l', '--leaflet', action='store_true', + dest='leaflet', + help="Set 0,0 point to north. For use with 'leaflet'. Requires -p raster. " + ) + p.add_option('-v', '--verbose', action='store_true', + dest='verbose', + help='Print status messages to stdout') + + # KML options + + g = OptionGroup(p, 'KML (Google Earth) options', + 'Options for generated Google Earth SuperOverlay metadata' + ) + g.add_option('-k', '--force-kml', dest='kml', + action='store_true', + help="Generate KML for Google Earth - default for 'geodetic' profile and 'raster' in EPSG:4326. For a dataset with different projection use with caution!" + ) + g.add_option('-n', '--no-kml', dest='kml', action='store_false' + , + help='Avoid automatic generation of KML files for EPSG:4326' + ) + g.add_option('-u', '--url', dest='url', + help='URL address where the generated tiles are going to be published' + ) + p.add_option_group(g) + + # HTML options + + g = OptionGroup(p, 'Web viewer options', + 'Options for generated HTML viewers a la Google Maps' + ) + g.add_option( + '-w', + '--webviewer', + dest='webviewer', + type='choice', + choices=webviewer_list, + help="Web viewer to generate (%s) - default 'all'" + % ','.join(webviewer_list), + ) + g.add_option('-t', '--title', dest='title', + help='Title of the map') + g.add_option('-c', '--copyright', dest='copyright', + help='Copyright for the map') + g.add_option('-g', '--googlekey', dest='googlekey', + help='Google Maps API key from http://code.google.com/apis/maps/signup.html' + ) + + (g.add_option('-b', '--bingkey', dest='bingkey', + help='Bing Maps API key from https://www.bingmapsportal.com/' + ), ) + p.add_option_group(g) + + # TODO: MapFile + TileIndexes per zoom level for efficient MapServer WMS + # g = OptionGroup(p, "WMS MapServer metadata", "Options for generated mapfile and tileindexes for MapServer") + # g.add_option("-i", "--tileindex", dest='wms', action="store_true" + # help="Generate tileindex and mapfile for MapServer (WMS)") + # p.add_option_group(g) + + p.set_defaults( + verbose=False, + profile='mercator', + kml=False, + url='', + webviewer='all', + copyright='', + resampling='average', + resume=False, + googlekey='INSERT_YOUR_KEY_HERE', + bingkey='INSERT_YOUR_KEY_HERE', + ) + + self.parser = p + + # ------------------------------------------------------------------------- + + def open_input(self): + """Initialization of the input raster, reprojection if necessary""" + + gdal.AllRegister() + + # Initialize necessary GDAL drivers + + self.out_drv = gdal.GetDriverByName(self.tiledriver) + self.mem_drv = gdal.GetDriverByName('MEM') + + if not self.out_drv: + raise Exception("The '%s' driver was not found, is it available in this GDAL build?" + , self.tiledriver) + if not self.mem_drv: + raise Exception("The 'MEM' driver was not found, is it available in this GDAL build?" + ) + + # Open the input file + + if self.input: + self.in_ds = gdal.Open(self.input, gdal.GA_ReadOnly) + else: + raise Exception('No input file was specified') + + if self.options.verbose: + print('Input file:', '( %sP x %sL - %s bands)' + % (self.in_ds.RasterXSize, self.in_ds.RasterYSize, + self.in_ds.RasterCount)) + + if not self.in_ds: + + # Note: GDAL prints the ERROR message too + + self.error("It is not possible to open the input file '%s'." + % self.input) + + # Read metadata from the input file + + if self.in_ds.RasterCount == 0: + self.error("Input file '%s' has no raster band" + % self.input) + + if self.in_ds.GetRasterBand(1).GetRasterColorTable(): + + # TODO: Process directly paletted dataset by generating VRT in memory + + self.error('Please convert this file to RGB/RGBA and run gdal2tiles on the result.' + , + """From paletted file you can create RGBA file (temp.vrt) by: +gdal_translate -of vrt -expand rgba %s temp.vrt +then run: +gdal2tiles temp.vrt""" + % self.input) + + # Get NODATA value + + self.in_nodata = [] + for i in range(1, self.in_ds.RasterCount + 1): + if self.in_ds.GetRasterBand(i).GetNoDataValue() != None: + self.in_nodata.append(self.in_ds.GetRasterBand(i).GetNoDataValue()) + if self.options.srcnodata: + nds = list(map(float, self.options.srcnodata.split(','))) + if len(nds) < self.in_ds.RasterCount: + self.in_nodata = (nds + * self.in_ds.RasterCount)[:self.in_ds.RasterCount] + else: + self.in_nodata = nds + + if self.options.verbose: + print('NODATA: %s' % self.in_nodata) + + # + # Here we should have RGBA input dataset opened in self.in_ds + # + + if self.options.verbose: + print ('Preprocessed file:', '( %sP x %sL - %s bands)' + % (self.in_ds.RasterXSize, self.in_ds.RasterYSize, + self.in_ds.RasterCount)) + + # Spatial Reference System of the input raster + + self.in_srs = None + + if self.options.s_srs: + self.in_srs = osr.SpatialReference() + self.in_srs.SetFromUserInput(self.options.s_srs) + self.in_srs_wkt = self.in_srs.ExportToWkt() + else: + self.in_srs_wkt = self.in_ds.GetProjection() + if not self.in_srs_wkt and self.in_ds.GetGCPCount() != 0: + self.in_srs_wkt = self.in_ds.GetGCPProjection() + if self.in_srs_wkt: + self.in_srs = osr.SpatialReference() + self.in_srs.ImportFromWkt(self.in_srs_wkt) + + # elif self.options.profile != 'raster': + # self.error("There is no spatial reference system info included in the input file.","You should run gdal2tiles with --s_srs EPSG:XXXX or similar.") + + # Spatial Reference System of tiles + + self.out_srs = osr.SpatialReference() + + if self.options.profile == 'mercator': + self.out_srs.ImportFromEPSG(900913) + elif self.options.profile == 'geodetic': + self.out_srs.ImportFromEPSG(4326) + else: + self.out_srs = self.in_srs + + # Are the reference systems the same? Reproject if necessary. + + self.out_ds = None + + if self.options.profile in ('mercator', 'geodetic'): + + if self.in_ds.GetGeoTransform() == ( + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + ) and self.in_ds.GetGCPCount() == 0: + self.error("There is no georeference - neither affine transformation (worldfile) nor GCPs. You can generate only 'raster' profile tiles." + , + "Either gdal2tiles with parameter -p 'raster' or use another GIS software for georeference e.g. gdal_transform -gcp / -a_ullr / -a_srs" + ) + + if self.in_srs: + + if self.in_srs.ExportToProj4() \ + != self.out_srs.ExportToProj4() \ + or self.in_ds.GetGCPCount() != 0: + + # Generation of VRT dataset in tile projection, default 'nearest neighbour' warping + + self.out_ds = gdal.AutoCreateWarpedVRT(self.in_ds, + self.in_srs_wkt, self.out_srs.ExportToWkt()) + + # TODO: HIGH PRIORITY: Correction of AutoCreateWarpedVRT according the max zoomlevel for correct direct warping!!! + + if self.options.verbose: + print("Warping of the raster by AutoCreateWarpedVRT (result saved into 'tiles.vrt')") + self.out_ds.GetDriver().CreateCopy('tiles.vrt', + self.out_ds) + + # Note: self.in_srs and self.in_srs_wkt contain still the non-warped reference system!!! + + # Correction of AutoCreateWarpedVRT for NODATA values + + if self.in_nodata != []: + import tempfile + tempfilename = tempfile.mktemp('-gdal2tiles.vrt' + ) + self.out_ds.GetDriver().CreateCopy(tempfilename, + self.out_ds) + + # open as a text file + + s = open(tempfilename).read() + + # Add the warping options + + s = s.replace("""""", + """ + + """) + + # replace BandMapping tag for NODATA bands.... + + for i in range(len(self.in_nodata)): + s = \ + s.replace("""""" + % (i + 1, i + 1), + """ + %i + 0 + %i + 0 + """ + % (i + 1, i + 1, self.in_nodata[i], + self.in_nodata[i])) # Or rewrite to white by: , 255 )) + + # save the corrected VRT + + open(tempfilename, 'w').write(s) + + # open by GDAL as self.out_ds + + self.out_ds = gdal.Open(tempfilename) # , gdal.GA_ReadOnly) + + # delete the temporary file + + os.unlink(tempfilename) + + # set NODATA_VALUE metadata + + self.out_ds.SetMetadataItem('NODATA_VALUES', + '%i %i %i' % (self.in_nodata[0], + self.in_nodata[1], self.in_nodata[2])) + + if self.options.verbose: + print("Modified warping result saved into 'tiles1.vrt'") + open('tiles1.vrt', 'w').write(s) + + # ----------------------------------- + # Correction of AutoCreateWarpedVRT for Mono (1 band) and RGB (3 bands) files without NODATA: + # equivalent of gdalwarp -dstalpha + + if self.in_nodata == [] and self.out_ds.RasterCount \ + in [1, 3]: + import tempfile + tempfilename = tempfile.mktemp('-gdal2tiles.vrt' + ) + self.out_ds.GetDriver().CreateCopy(tempfilename, + self.out_ds) + + # open as a text file + + s = open(tempfilename).read() + + # Add the warping options + + s = s.replace("""""", + """ + Alpha + + """ + % (self.out_ds.RasterCount + 1)) + s = s.replace("""""", + """%i + """ + % (self.out_ds.RasterCount + 1)) + s = s.replace("""""", + """ + """ + ) + + # save the corrected VRT + + open(tempfilename, 'w').write(s) + + # open by GDAL as self.out_ds + + self.out_ds = gdal.Open(tempfilename) # , gdal.GA_ReadOnly) + + # delete the temporary file + + os.unlink(tempfilename) + + if self.options.verbose: + print("Modified -dstalpha warping result saved into 'tiles1.vrt'") + open('tiles1.vrt', 'w').write(s) + s = ''' + ''' + else: + + self.error('Input file has unknown SRS.', + 'Use --s_srs ESPG:xyz (or similar) to provide source reference system.' + ) + + if self.out_ds and self.options.verbose: + print ('Projected file:', 'tiles.vrt', + '( %sP x %sL - %s bands)' + % (self.out_ds.RasterXSize, + self.out_ds.RasterYSize, + self.out_ds.RasterCount)) + + if not self.out_ds: + self.out_ds = self.in_ds + + # + # Here we should have a raster (out_ds) in the correct Spatial Reference system + # + + # Get alpha band (either directly or from NODATA value) + + self.alphaband = self.out_ds.GetRasterBand(1).GetMaskBand() + if self.alphaband.GetMaskFlags() & gdal.GMF_ALPHA \ + or self.out_ds.RasterCount == 4 or self.out_ds.RasterCount \ + == 2: + + # TODO: Better test for alpha band in the dataset + + self.dataBandsCount = self.out_ds.RasterCount - 1 + else: + self.dataBandsCount = self.out_ds.RasterCount + + # KML test + + self.isepsg4326 = False + srs4326 = osr.SpatialReference() + srs4326.ImportFromEPSG(4326) + if self.out_srs and srs4326.ExportToProj4() \ + == self.out_srs.ExportToProj4(): + self.kml = True + self.isepsg4326 = True + if self.options.verbose: + print('KML autotest OK!') + + # Read the georeference + + self.out_gt = self.out_ds.GetGeoTransform() + + # originX, originY = self.out_gt[0], self.out_gt[3] + # pixelSize = self.out_gt[1] # = self.out_gt[5] + + # Test the size of the pixel + + # MAPTILER - COMMENTED + # if self.out_gt[1] != (-1 * self.out_gt[5]) and self.options.profile != 'raster': + # TODO: Process corectly coordinates with are have swichted Y axis (display in OpenLayers too) + # self.error("Size of the pixel in the output differ for X and Y axes.") + + # Report error in case rotation/skew is in geotransform (possible only in 'raster' profile) + + if (self.out_gt[2], self.out_gt[4]) != (0, 0): + self.error('Georeference of the raster contains rotation or skew. Such raster is not supported. Please use gdalwarp first.' + ) + + # TODO: Do the warping in this case automaticaly + + # + # Here we expect: pixel is square, no rotation on the raster + # + + # Output Bounds - coordinates in the output SRS + + self.ominx = self.out_gt[0] + self.omaxx = self.out_gt[0] + self.out_ds.RasterXSize \ + * self.out_gt[1] + self.omaxy = self.out_gt[3] + self.ominy = self.out_gt[3] - self.out_ds.RasterYSize \ + * self.out_gt[1] + + # Note: maybe round(x, 14) to avoid the gdal_translate behaviour, when 0 becomes -1e-15 + + if self.options.verbose: + print ('Bounds (output srs):', round(self.ominx, 13), + self.ominy, self.omaxx, self.omaxy) + + # + # Calculating ranges for tiles in different zoom levels + # + + if self.options.profile == 'mercator': + + self.mercator = GlobalMercator() # from globalmaptiles.py + + # Function which generates SWNE in LatLong for given tile + + self.tileswne = self.mercator.TileLatLonBounds + + # Generate table with min max tile coordinates for all zoomlevels + + self.tminmax = list(range(0, 32)) + for tz in range(0, 32): + (tminx, tminy) = self.mercator.MetersToTile(self.ominx, + self.ominy, tz) + (tmaxx, tmaxy) = self.mercator.MetersToTile(self.omaxx, + self.omaxy, tz) + + # crop tiles extending world limits (+-180,+-90) + + (tminx, tminy) = (max(0, tminx), max(0, tminy)) + (tmaxx, tmaxy) = (min(2 ** tz - 1, tmaxx), min(2 ** tz + - 1, tmaxy)) + self.tminmax[tz] = (tminx, tminy, tmaxx, tmaxy) + + # TODO: Maps crossing 180E (Alaska?) + + # Get the minimal zoom level (map covers area equivalent to one tile) + + if self.tminz == None: + self.tminz = \ + self.mercator.ZoomForPixelSize(self.out_gt[1] + * max(self.out_ds.RasterXSize, + self.out_ds.RasterYSize) / float(self.tilesize)) + + # Get the maximal zoom level (closest possible zoom level up on the resolution of raster) + + if self.tmaxz == None: + self.tmaxz = \ + self.mercator.ZoomForPixelSize(self.out_gt[1]) + + if self.options.verbose: + print ('Bounds (latlong):', + self.mercator.MetersToLatLon(self.ominx, + self.ominy), + self.mercator.MetersToLatLon(self.omaxx, + self.omaxy)) + print ('MinZoomLevel:', self.tminz) + print ('MaxZoomLevel:', self.tmaxz, '(', + self.mercator.Resolution(self.tmaxz), ')') + + if self.options.profile == 'geodetic': + + self.geodetic = GlobalGeodetic(self.options.tmscompatible) # from globalmaptiles.py + + # Function which generates SWNE in LatLong for given tile + + self.tileswne = self.geodetic.TileLatLonBounds + + # Generate table with min max tile coordinates for all zoomlevels + + self.tminmax = list(range(0, 32)) + for tz in range(0, 32): + (tminx, tminy) = self.geodetic.LonLatToTile(self.ominx, + self.ominy, tz) + (tmaxx, tmaxy) = self.geodetic.LonLatToTile(self.omaxx, + self.omaxy, tz) + + # crop tiles extending world limits (+-180,+-90) + + (tminx, tminy) = (max(0, tminx), max(0, tminy)) + (tmaxx, tmaxy) = (min(2 ** (tz + 1) - 1, tmaxx), min(2 + ** tz - 1, tmaxy)) + self.tminmax[tz] = (tminx, tminy, tmaxx, tmaxy) + + # TODO: Maps crossing 180E (Alaska?) + + # Get the maximal zoom level (closest possible zoom level up on the resolution of raster) + + if self.tminz == None: + self.tminz = \ + self.geodetic.ZoomForPixelSize(self.out_gt[1] + * max(self.out_ds.RasterXSize, + self.out_ds.RasterYSize) / float(self.tilesize)) + + # Get the maximal zoom level (closest possible zoom level up on the resolution of raster) + + if self.tmaxz == None: + self.tmaxz = \ + self.geodetic.ZoomForPixelSize(self.out_gt[1]) + + if self.options.verbose: + print ('Bounds (latlong):', self.ominx, self.ominy, + self.omaxx, self.omaxy) + + if self.options.profile == 'raster': + + log2 = lambda x: math.log10(x) / math.log10(2) # log2 (base 2 logarithm) + + self.nativezoom = \ + int(max(math.ceil(log2(self.out_ds.RasterXSize + / float(self.tilesize))), + math.ceil(log2(self.out_ds.RasterYSize + / float(self.tilesize))))) + + if int(self.tmaxz or 0) < self.nativezoom: + self.tmaxz = self.nativezoom + + if self.options.verbose: + print ('Native zoom of the raster:', self.nativezoom) + + # Get the minimal zoom level (whole raster in one tile) + + if self.tminz == None: + self.tminz = 0 + + # Get the maximal zoom level (native resolution of the raster) + + if self.tmaxz == None: + self.tmaxz = self.nativezoom + + # Generate table with min max tile coordinates for all zoomlevels + + self.tminmax = list(range(0, self.tmaxz + 1)) + self.tsize = list(range(0, self.tmaxz + 1)) + for tz in range(0, self.tmaxz + 1): + tsize = 2.0 ** (self.nativezoom - tz) * self.tilesize + (tminx, tminy) = (0, 0) + tmaxx = int(math.ceil(self.out_ds.RasterXSize / tsize)) \ + - 1 + tmaxy = int(math.ceil(self.out_ds.RasterYSize / tsize)) \ + - 1 + self.tsize[tz] = math.ceil(tsize) + self.tminmax[tz] = (tminx, tminy, tmaxx, tmaxy) + + # Function which generates SWNE in LatLong for given tile + + if self.kml and self.in_srs_wkt: + self.ct = osr.CoordinateTransformation(self.in_srs, + srs4326) + + def rastertileswne(x, y, z): + pixelsizex = 2 ** (self.tmaxz - z) * self.out_gt[1] # X-pixel size in level + pixelsizey = 2 ** (self.tmaxz - z) * self.out_gt[1] # Y-pixel size in level (usually -1*pixelsizex) + west = self.out_gt[0] + x * self.tilesize \ + * pixelsizex + east = west + self.tilesize * pixelsizex + south = self.ominy + y * self.tilesize * pixelsizex + north = south + self.tilesize * pixelsizex + if not self.isepsg4326: + + # Transformation to EPSG:4326 (WGS84 datum) + + (west, south) = self.ct.TransformPoint(west, + south)[:2] + (east, north) = self.ct.TransformPoint(east, + north)[:2] + return (south, west, north, east) + + self.tileswne = rastertileswne + else: + self.tileswne = lambda x, y, z: (0, 0, 0, 0) + + # ------------------------------------------------------------------------- + + def generate_metadata(self): + """Generation of main metadata files and HTML viewers (metadata related to particular tiles are generated during the tile processing).""" + + if not os.path.exists(self.output): + os.makedirs(self.output) + + if self.options.profile == 'mercator': + + (south, west) = self.mercator.MetersToLatLon(self.ominx, + self.ominy) + (north, east) = self.mercator.MetersToLatLon(self.omaxx, + self.omaxy) + (south, west) = (max(-85.05112878, south), max(-180.0, + west)) + (north, east) = (min(85.05112878, north), min(180.0, east)) + self.swne = (south, west, north, east) + + # Generate googlemaps.html + + if self.options.webviewer in ('all', 'google') \ + and self.options.profile == 'mercator': + if not self.options.resume \ + or not os.path.exists(os.path.join(self.output, + 'googlemaps.html')): + f = open(os.path.join(self.output, 'googlemaps.html' + ), 'w') + f.write(self.generate_googlemaps()) + f.close() + + # Generate openlayers.html + + if self.options.webviewer in ('all', 'openlayers'): + if not self.options.resume \ + or not os.path.exists(os.path.join(self.output, + 'openlayers.html')): + f = open(os.path.join(self.output, 'openlayers.html' + ), 'w') + f.write(self.generate_openlayers()) + f.close() + elif self.options.profile == 'geodetic': + + (west, south) = (self.ominx, self.ominy) + (east, north) = (self.omaxx, self.omaxy) + (south, west) = (max(-90.0, south), max(-180.0, west)) + (north, east) = (min(90.0, north), min(180.0, east)) + self.swne = (south, west, north, east) + + # Generate openlayers.html + + if self.options.webviewer in ('all', 'openlayers'): + if not self.options.resume \ + or not os.path.exists(os.path.join(self.output, + 'openlayers.html')): + f = open(os.path.join(self.output, 'openlayers.html' + ), 'w') + f.write(self.generate_openlayers()) + f.close() + elif self.options.profile == 'raster': + + (west, south) = (self.ominx, self.ominy) + (east, north) = (self.omaxx, self.omaxy) + + self.swne = (south, west, north, east) + + # Generate openlayers.html + + if self.options.webviewer in ('all', 'openlayers'): + if not self.options.resume \ + or not os.path.exists(os.path.join(self.output, + 'openlayers.html')): + f = open(os.path.join(self.output, 'openlayers.html' + ), 'w') + f.write(self.generate_openlayers()) + f.close() + + # Generate tilemapresource.xml. + + if not self.options.resume \ + or not os.path.exists(os.path.join(self.output, + 'tilemapresource.xml')): + f = open(os.path.join(self.output, 'tilemapresource.xml'), + 'w') + f.write(self.generate_tilemapresource()) + f.close() + + if self.kml: + + # TODO: Maybe problem for not automatically generated tminz + # The root KML should contain links to all tiles in the tminz level + + children = [] + (xmin, ymin, xmax, ymax) = self.tminmax[self.tminz] + for x in range(xmin, xmax + 1): + for y in range(ymin, ymax + 1): + children.append([x, y, self.tminz]) + + # Generate Root KML + + if self.kml: + if not self.options.resume \ + or not os.path.exists(os.path.join(self.output, + 'doc.kml')): + f = open(os.path.join(self.output, 'doc.kml'), 'w') + f.write(self.generate_kml(None, None, None, + children)) + f.close() + + # ------------------------------------------------------------------------- + + def generate_base_tiles(self): + """Generation of the base tiles (the lowest in the pyramid) directly from the input raster""" + + print('Generating Base Tiles:') + + if self.options.verbose: + + # mx, my = self.out_gt[0], self.out_gt[3] # OriginX, OriginY + # px, py = self.mercator.MetersToPixels( mx, my, self.tmaxz) + # print("Pixel coordinates:", px, py, (mx, my)) + + print('') + print('Tiles generated from the max zoom level:') + print('----------------------------------------') + print('') + + # Set the bounds + + (tminx, tminy, tmaxx, tmaxy) = self.tminmax[self.tmaxz] + + # Just the center tile + # tminx = tminx+ (tmaxx - tminx)/2 + # tminy = tminy+ (tmaxy - tminy)/2 + # tmaxx = tminx + # tmaxy = tminy + + ds = self.out_ds + tilebands = self.dataBandsCount + 1 + querysize = self.querysize + + if self.options.verbose: + print ('dataBandsCount: ', self.dataBandsCount) + print ('tilebands: ', tilebands) + + # print(tminx, tminy, tmaxx, tmaxy) + + tcount = (1 + abs(tmaxx - tminx)) * (1 + abs(tmaxy - tminy)) + + # print(tcount) + + ti = 0 + + tz = self.tmaxz + yrange = range(tmaxy, tminy - 1, -1) + if self.options.leaflet: + yrange = range(tminy, tmaxy + 1) + + for ty in yrange: + for tx in range(tminx, tmaxx + 1): + + if self.stopped: + break + ti += 1 + tilefilename = os.path.join(self.output, str(tz), + str(tx), '%s.%s' % (ty, self.tileext)) + if self.options.verbose: + print (ti, '/', tcount, tilefilename) # , "( TileMapService: z / x / y )" + + if self.options.resume and os.path.exists(tilefilename): + if self.options.verbose: + print('Tile generation skiped because of --resume') + else: + self.progressbar(ti / float(tcount)) + continue + + # Create directories for the tile + + if not os.path.exists(os.path.dirname(tilefilename)): + os.makedirs(os.path.dirname(tilefilename)) + + if self.options.profile == 'mercator': + + # Tile bounds in EPSG:900913 + + b = self.mercator.TileBounds(tx, ty, tz) + elif self.options.profile == 'geodetic': + b = self.geodetic.TileBounds(tx, ty, tz) + + # print("\tgdalwarp -ts 256 256 -te %s %s %s %s %s %s_%s_%s.tif" % ( b[0], b[1], b[2], b[3], "tiles.vrt", tz, tx, ty)) + + # Don't scale up by nearest neighbour, better change the querysize + # to the native resolution (and return smaller query tile) for scaling + + if self.options.profile in ('mercator', 'geodetic'): + (rb, wb) = self.geo_query(ds, b[0], b[3], b[2], + b[1]) + nativesize = wb[0] + wb[2] # Pixel size in the raster covering query geo extent + if self.options.verbose: + print ('\tNative Extent (querysize', + nativesize, '): ', rb, wb) + + # Tile bounds in raster coordinates for ReadRaster query + + (rb, wb) = self.geo_query( + ds, + b[0], + b[3], + b[2], + b[1], + querysize=querysize, + ) + + (rx, ry, rxsize, rysize) = rb + (wx, wy, wxsize, wysize) = wb + else: + + # 'raster' profile: + + tsize = int(self.tsize[tz]) # tilesize in raster coordinates for actual zoom + xsize = self.out_ds.RasterXSize # size of the raster in pixels + ysize = self.out_ds.RasterYSize + if tz >= self.nativezoom: + querysize = self.tilesize # int(2**(self.nativezoom-tz) * self.tilesize) + + rx = tx * tsize + rxsize = 0 + if tx == tmaxx: + rxsize = xsize % tsize + if rxsize == 0: + rxsize = tsize + + rysize = 0 + if ty == tmaxy: + rysize = ysize % tsize + if rysize == 0: + rysize = tsize + if self.options.leaflet: + ry = ty * tsize + else: + ry = ysize - ty * tsize - rysize + + (wx, wy) = (0, 0) + (wxsize, wysize) = (int(rxsize / float(tsize) + * self.tilesize), int(rysize / float(tsize) + * self.tilesize)) + if not self.options.leaflet: + if wysize != self.tilesize: + wy = self.tilesize - wysize + + if self.options.verbose: + print ('\tReadRaster Extent: ', (rx, ry, rxsize, + rysize), (wx, wy, wxsize, wysize)) + + # Query is in 'nearest neighbour' but can be bigger in then the tilesize + # We scale down the query to the tilesize by supplied algorithm. + + # Tile dataset in memory + + dstile = self.mem_drv.Create('', self.tilesize, + self.tilesize, tilebands) + data = ds.ReadRaster( + rx, + ry, + rxsize, + rysize, + wxsize, + wysize, + band_list=list(range(1, self.dataBandsCount + 1)), + ) + alpha = self.alphaband.ReadRaster( + rx, + ry, + rxsize, + rysize, + wxsize, + wysize, + ) + + if self.tilesize == querysize: + + # Use the ReadRaster result directly in tiles ('nearest neighbour' query) + + dstile.WriteRaster( + wx, + wy, + wxsize, + wysize, + data, + band_list=list(range(1, self.dataBandsCount + + 1)), + ) + dstile.WriteRaster( + wx, + wy, + wxsize, + wysize, + alpha, + band_list=[tilebands], + ) + else: + + # Note: For source drivers based on WaveLet compression (JPEG2000, ECW, MrSID) + # the ReadRaster function returns high-quality raster (not ugly nearest neighbour) + # TODO: Use directly 'near' for WaveLet files + # Big ReadRaster query in memory scaled to the tilesize - all but 'near' algo + + dsquery = self.mem_drv.Create('', querysize, + querysize, tilebands) + + # TODO: fill the null value in case a tile without alpha is produced (now only png tiles are supported) + # for i in range(1, tilebands+1): + # dsquery.GetRasterBand(1).Fill(tilenodata) + + dsquery.WriteRaster( + wx, + wy, + wxsize, + wysize, + data, + band_list=list(range(1, self.dataBandsCount + + 1)), + ) + dsquery.WriteRaster( + wx, + wy, + wxsize, + wysize, + alpha, + band_list=[tilebands], + ) + + self.scale_query_to_tile(dsquery, dstile, + tilefilename) + del dsquery + + del data + + if self.options.resampling != 'antialias': + + # Write a copy of tile to png/jpg + + self.out_drv.CreateCopy(tilefilename, dstile, + strict=0) + + del dstile + + # Create a KML file for this tile. + + if self.kml: + kmlfilename = os.path.join(self.output, str(tz), + str(tx), '%d.kml' % ty) + if not self.options.resume \ + or not os.path.exists(kmlfilename): + f = open(kmlfilename, 'w') + f.write(self.generate_kml(tx, ty, tz)) + f.close() + + if not self.options.verbose: + self.progressbar(ti / float(tcount)) + + # ------------------------------------------------------------------------- + + def generate_overview_tiles(self): + """Generation of the overview tiles (higher in the pyramid) based on existing tiles""" + + print('Generating Overview Tiles:') + + tilebands = self.dataBandsCount + 1 + + # Usage of existing tiles: from 4 underlying tiles generate one as overview. + + tcount = 0 + for tz in range(self.tmaxz - 1, self.tminz - 1, -1): + (tminx, tminy, tmaxx, tmaxy) = self.tminmax[tz] + tcount += (1 + abs(tmaxx - tminx)) * (1 + abs(tmaxy + - tminy)) + + ti = 0 + + # querysize = tilesize * 2 + + for tz in range(self.tmaxz - 1, self.tminz - 1, -1): + (tminx, tminy, tmaxx, tmaxy) = self.tminmax[tz] + yrange = range(tmaxy, tminy - 1, -1) + if self.options.leaflet: + yrange = range(tminy, tmaxy + 1) + for ty in yrange: + for tx in range(tminx, tmaxx + 1): + + if self.stopped: + break + + ti += 1 + tilefilename = os.path.join(self.output, str(tz), + str(tx), '%s.%s' % (ty, self.tileext)) + + if self.options.verbose: + print (ti, '/', tcount, tilefilename) # , "( TileMapService: z / x / y )" + + if self.options.resume \ + and os.path.exists(tilefilename): + if self.options.verbose: + print('Tile generation skiped because of --resume') + else: + self.progressbar(ti / float(tcount)) + continue + + # Create directories for the tile + + if not os.path.exists(os.path.dirname(tilefilename)): + os.makedirs(os.path.dirname(tilefilename)) + + dsquery = self.mem_drv.Create('', 2 + * self.tilesize, 2 * self.tilesize, + tilebands) + + # TODO: fill the null value + # for i in range(1, tilebands+1): + # dsquery.GetRasterBand(1).Fill(tilenodata) + + dstile = self.mem_drv.Create('', self.tilesize, + self.tilesize, tilebands) + + # TODO: Implement more clever walking on the tiles with cache functionality + # probably walk should start with reading of four tiles from top left corner + # Hilbert curve + + children = [] + + # Read the tiles and write them to query window + + for y in range(2 * ty, 2 * ty + 2): + for x in range(2 * tx, 2 * tx + 2): + (minx, miny, maxx, maxy) = self.tminmax[tz + + 1] + if x >= minx and x <= maxx and y >= miny \ + and y <= maxy: + dsquerytile = \ + gdal.Open(os.path.join(self.output, + str(tz + 1), str(x), '%s.%s' + % (y, self.tileext)), + gdal.GA_ReadOnly) + + if self.options.leaflet: + if ty: + tileposy = y % (2 * ty) \ + * self.tilesize + elif ty == 0 and y == 1: + tileposy = self.tilesize + else: + tileposy = 0 + else: + if ty == 0 and y == 1 or ty != 0 \ + and y % (2 * ty) != 0: + tileposy = 0 + else: + tileposy = self.tilesize + + if tx: + tileposx = x % (2 * tx) \ + * self.tilesize + elif tx == 0 and x == 1: + tileposx = self.tilesize + else: + tileposx = 0 + dsquery.WriteRaster( + tileposx, + tileposy, + self.tilesize, + self.tilesize, + dsquerytile.ReadRaster(0, 0, + self.tilesize, self.tilesize), + band_list=list(range(1, tilebands + + 1)), + ) + children.append([x, y, tz + 1]) + + self.scale_query_to_tile(dsquery, dstile, + tilefilename) + + # Write a copy of tile to png/jpg + + if self.options.resampling != 'antialias': + + # Write a copy of tile to png/jpg + + self.out_drv.CreateCopy(tilefilename, dstile, + strict=0) + + if self.options.verbose: + print ( + '\tbuild from zoom', + tz + 1, + ' tiles:', + (2 * tx, 2 * ty), + (2 * tx + 1, 2 * ty), + (2 * tx, 2 * ty + 1), + (2 * tx + 1, 2 * ty + 1), + ) + + # Create a KML file for this tile. + + if self.kml: + f = open(os.path.join(self.output, + '%d/%d/%d.kml' % (tz, tx, ty)), 'w') + f.write(self.generate_kml(tx, ty, tz, children)) + f.close() + + if not self.options.verbose: + self.progressbar(ti / float(tcount)) + + # ------------------------------------------------------------------------- + + def geo_query( + self, + ds, + ulx, + uly, + lrx, + lry, + querysize=0, + ): + """For given dataset and query in cartographic coordinates + returns parameters for ReadRaster() in raster coordinates and + x/y shifts (for border tiles). If the querysize is not given, the + extent is returned in the native resolution of dataset ds.""" + + geotran = ds.GetGeoTransform() + rx = int((ulx - geotran[0]) / geotran[1] + 0.001) + ry = int((uly - geotran[3]) / geotran[5] + 0.001) + rxsize = int((lrx - ulx) / geotran[1] + 0.5) + rysize = int((lry - uly) / geotran[5] + 0.5) + + if not querysize: + (wxsize, wysize) = (rxsize, rysize) + else: + (wxsize, wysize) = (querysize, querysize) + + # Coordinates should not go out of the bounds of the raster + + wx = 0 + if rx < 0: + rxshift = abs(rx) + wx = int(wxsize * (float(rxshift) / rxsize)) + wxsize = wxsize - wx + rxsize = rxsize - int(rxsize * (float(rxshift) / rxsize)) + rx = 0 + if rx + rxsize > ds.RasterXSize: + wxsize = int(wxsize * (float(ds.RasterXSize - rx) / rxsize)) + rxsize = ds.RasterXSize - rx + + wy = 0 + if ry < 0: + ryshift = abs(ry) + wy = int(wysize * (float(ryshift) / rysize)) + wysize = wysize - wy + rysize = rysize - int(rysize * (float(ryshift) / rysize)) + ry = 0 + if ry + rysize > ds.RasterYSize: + wysize = int(wysize * (float(ds.RasterYSize - ry) / rysize)) + rysize = ds.RasterYSize - ry + + return ((rx, ry, rxsize, rysize), (wx, wy, wxsize, wysize)) + + # ------------------------------------------------------------------------- + + def scale_query_to_tile( + self, + dsquery, + dstile, + tilefilename='', + ): + """Scales down query dataset to the tile dataset""" + + querysize = dsquery.RasterXSize + tilesize = dstile.RasterXSize + tilebands = dstile.RasterCount + + if self.options.resampling == 'average': + + # Function: gdal.RegenerateOverview() + + for i in range(1, tilebands + 1): + + # Black border around NODATA + # if i != 4: + # dsquery.GetRasterBand(i).SetNoDataValue(0) + + res = gdal.RegenerateOverview(dsquery.GetRasterBand(i), + dstile.GetRasterBand(i), 'average') + if res != 0: + self.error('RegenerateOverview() failed on %s, error %d' + % (tilefilename, res)) + elif self.options.resampling == 'antialias': + + # Scaling by PIL (Python Imaging Library) - improved Lanczos + + array = numpy.zeros((querysize, querysize, tilebands), + numpy.uint8) + for i in range(tilebands): + array[:, :, i] = \ + gdalarray.BandReadAsArray(dsquery.GetRasterBand(i + + 1), 0, 0, querysize, querysize) + im = Image.fromarray(array, 'RGBA') # Always four bands + im1 = im.resize((tilesize, tilesize), Image.ANTIALIAS) + if os.path.exists(tilefilename): + im0 = Image.open(tilefilename) + im1 = Image.composite(im1, im0, im1) + im1.save(tilefilename, self.tiledriver) + else: + + # Other algorithms are implemented by gdal.ReprojectImage(). + + dsquery.SetGeoTransform(( + 0.0, + tilesize / float(querysize), + 0.0, + 0.0, + 0.0, + tilesize / float(querysize), + )) + dstile.SetGeoTransform(( + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + )) + + res = gdal.ReprojectImage(dsquery, dstile, None, None, + self.resampling) + if res != 0: + self.error('ReprojectImage() failed on %s, error %d' + % (tilefilename, res)) + + # ------------------------------------------------------------------------- + + def generate_tilemapresource(self): + """ + Template for tilemapresource.xml. Returns filled string. Expected variables: + title, north, south, east, west, isepsg4326, projection, publishurl, + zoompixels, tilesize, tileformat, profile + """ + + args = {} + args['title'] = self.options.title + (args['south'], args['west'], args['north'], args['east']) = \ + self.swne + args['tilesize'] = self.tilesize + args['tileformat'] = self.tileext + args['publishurl'] = self.options.url + args['profile'] = self.options.profile + + if self.options.profile == 'mercator': + args['srs'] = 'EPSG:900913' + elif self.options.profile == 'geodetic': + args['srs'] = 'EPSG:4326' + elif self.options.s_srs: + args['srs'] = self.options.s_srs + elif self.out_srs: + args['srs'] = self.out_srs.ExportToWkt() + else: + args['srs'] = '' + + s = \ + """ + + %(title)s + + %(srs)s + + + + +""" \ + % args + for z in range(self.tminz, self.tmaxz + 1): + if self.options.profile == 'raster': + s += \ + """ \n""" \ + % (args['publishurl'], z, 2 ** (self.nativezoom + - z) * self.out_gt[1], z) + elif self.options.profile == 'mercator': + s += \ + """ \n""" \ + % (args['publishurl'], z, 156543.0339 / 2 ** z, z) + elif self.options.profile == 'geodetic': + s += \ + """ \n""" \ + % (args['publishurl'], z, 0.703125 / 2 ** z, z) + s += """ + + """ + return s + + # ------------------------------------------------------------------------- + + def generate_kml( + self, + tx, + ty, + tz, + children=[], + **args + ): + """ + Template for the KML. Returns filled string. + """ + + (args['tx'], args['ty'], args['tz']) = (tx, ty, tz) + args['tileformat'] = self.tileext + if 'tilesize' not in args: + args['tilesize'] = self.tilesize + + if 'minlodpixels' not in args: + args['minlodpixels'] = int(args['tilesize'] / 2) # / 2.56) # default 128 + if 'maxlodpixels' not in args: + args['maxlodpixels'] = int(args['tilesize'] * 8) # 1.7) # default 2048 (used to be -1) + if children == []: + args['maxlodpixels'] = -1 + + if tx == None: + tilekml = False + args['title'] = self.options.title + else: + tilekml = True + args['title'] = '%d/%d/%d.kml' % (tz, tx, ty) + (args['south'], args['west'], args['north'], args['east' + ]) = self.tileswne(tx, ty, tz) + + if tx == 0: + args['drawOrder'] = 2 * tz + 1 + elif tx != None: + args['drawOrder'] = 2 * tz + else: + args['drawOrder'] = 0 + + url = self.options.url + if not url: + if tilekml: + url = '../../' + else: + url = '' + + s = \ + """ + + + %(title)s + + """ \ + % args + if tilekml: + s += \ + """ + + + %(north).14f + %(south).14f + %(east).14f + %(west).14f + + + %(minlodpixels)d + %(maxlodpixels)d + + + + %(drawOrder)d + + %(ty)d.%(tileformat)s + + + %(north).14f + %(south).14f + %(east).14f + %(west).14f + + + """ \ + % args + + for (cx, cy, cz) in children: + (csouth, cwest, cnorth, ceast) = self.tileswne(cx, cy, cz) + s += \ + """ + + %d/%d/%d.%s + + + %.14f + %.14f + %.14f + %.14f + + + %d + -1 + + + + %s%d/%d/%d.kml + onRegion + + + + """ \ + % ( + cz, + cx, + cy, + args['tileformat'], + cnorth, + csouth, + ceast, + cwest, + args['minlodpixels'], + url, + cz, + cx, + cy, + ) + + s += """ + + """ + return s + + # ------------------------------------------------------------------------- + + def generate_googlemaps(self): + """ + Template for googlemaps.html implementing Overlay of tiles for 'mercator' profile. + It returns filled string. Expected variables: + title, googlemapskey, north, south, east, west, minzoom, maxzoom, tilesize, tileformat, publishurl + """ + + args = {} + args['title'] = self.options.title + args['googlemapskey'] = self.options.googlekey + (args['south'], args['west'], args['north'], args['east']) = \ + self.swne + args['minzoom'] = self.tminz + args['maxzoom'] = self.tmaxz + args['tilesize'] = self.tilesize + args['tileformat'] = self.tileext + args['publishurl'] = self.options.url + args['copyright'] = self.options.copyright + + s = \ + """ + + + %(title)s + + + + + + + + +
Generated by MapTiler/GDAL2Tiles, Copyright © 2008 Klokan Petr Pridal, GDAL & OSGeo GSoC + +
+
+ + + """ \ + % args + + return s + + # ------------------------------------------------------------------------- + + def generate_openlayers(self): + """ + Template for openlayers.html implementing overlay of available Spherical Mercator layers. + + It returns filled string. Expected variables: + title, bingkey, north, south, east, west, minzoom, maxzoom, tilesize, tileformat, publishurl + """ + + args = {} + args['title'] = self.options.title + args['bingkey'] = self.options.bingkey + (args['south'], args['west'], args['north'], args['east']) = \ + self.swne + args['minzoom'] = self.tminz + args['maxzoom'] = self.tmaxz + args['tilesize'] = self.tilesize + args['tileformat'] = self.tileext + args['publishurl'] = self.options.url + args['copyright'] = self.options.copyright + if self.options.tmscompatible: + args['tmsoffset'] = '-1' + else: + args['tmsoffset'] = '' + if self.options.profile == 'raster': + args['rasterzoomlevels'] = self.tmaxz + 1 + args['rastermaxresolution'] = 2 ** self.nativezoom \ + * self.out_gt[1] + + s = \ + """ + + %(title)s + + """ \ + % args + + if self.options.profile == 'mercator': + s += \ + """ + """ \ + % args + + s += \ + """ + + + + + +
Generated by MapTiler/GDAL2Tiles, Copyright © 2008 Klokan Petr Pridal, GDAL & OSGeo GSoC + +
+
+ + + """ \ + % args + + return s + + +# ============================================================================= +# ============================================================================= +# ============================================================================= + +if __name__ == '__main__': + argv = gdal.GeneralCmdLineProcessor(sys.argv) + if argv: + gdal2tiles = GDAL2Tiles(argv[1:]) + gdal2tiles.process() diff --git a/index.html b/index.html new file mode 100644 index 0000000..5a79117 --- /dev/null +++ b/index.html @@ -0,0 +1,181 @@ + + + + + + + + + Whats On My Laptop - TheGreydiamond + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + \ No newline at end of file diff --git a/rastercoords.js b/rastercoords.js new file mode 100644 index 0000000..20e572b --- /dev/null +++ b/rastercoords.js @@ -0,0 +1,90 @@ +/** + * leaflet plugin for plain image map projection + * @copyright 2016- commenthol + * @license MIT + */ +/* globals define */ +/* eslint no-var:off */ + +;(function (factory) { + var L + if (typeof define === 'function' && define.amd) { + // AMD + define(['leaflet'], factory) + } else if (typeof module !== 'undefined') { + // Node/CommonJS + L = require('leaflet') + module.exports = factory(L) + } else { + // Browser globals + if (typeof window.L === 'undefined') { + throw new Error('Leaflet must be loaded first') + } + factory(window.L) + } +}(function (L) { + /** + * L.RasterCoords + * @param {L.map} map - the map used + * @param {Array} imgsize - [ width, height ] image dimensions + * @param {Number} [tilesize] - tilesize in pixels. Default=256 + */ + L.RasterCoords = function (map, imgsize, tilesize) { + this.map = map + this.width = imgsize[0] + this.height = imgsize[1] + this.tilesize = tilesize || 256 + this.zoom = this.zoomLevel() + if (this.width && this.height) { + this.setMaxBounds() + } + } + + L.RasterCoords.prototype = { + /** + * calculate accurate zoom level for the given image size + */ + zoomLevel: function () { + return Math.ceil( + Math.log( + Math.max(this.width, this.height) / + this.tilesize + ) / Math.log(2) + ) + }, + /** + * unproject `coords` to the raster coordinates used by the raster image projection + * @param {Array} coords - [ x, y ] + * @return {L.LatLng} - internal coordinates + */ + unproject: function (coords) { + return this.map.unproject(coords, this.zoom) + }, + /** + * project `coords` back to image coordinates + * @param {Array} coords - [ x, y ] + * @return {L.LatLng} - image coordinates + */ + project: function (coords) { + return this.map.project(coords, this.zoom) + }, + /** + * get the max bounds of the image + */ + getMaxBounds: function () { + var southWest = this.unproject([0, this.height]) + var northEast = this.unproject([this.width, 0]) + return new L.LatLngBounds(southWest, northEast) + }, + /** + * sets the max bounds on map + */ + setMaxBounds: function () { + var bounds = this.getMaxBounds() + this.map.setMaxBounds(bounds) + } + } + + return L.RasterCoords +})) +; // eslint-disable-line semi diff --git a/static/leaflet-editable-polyline.js b/static/leaflet-editable-polyline.js new file mode 100644 index 0000000..9fd6323 --- /dev/null +++ b/static/leaflet-editable-polyline.js @@ -0,0 +1,589 @@ +L.Polyline.polylineEditor = L.Polyline.extend({ + _prepareMapIfNeeded: function() { + var that = this; + that._changed = false; + + if(this._map._editablePolylines != null) { + return + } + + // Container for all editable polylines on this map: + this._map._editablePolylines = []; + this._map._editablePolylinesEnabled = true; + + // Click anywhere on map to add a new point-polyline: + if(this._options.newPolylines) { + console.log('click na map'); + that._map.on('dblclick', function(event) { + console.log('click, target=' + (event.target == that._map) + ' type=' + event.type); + if(that._map.isEditablePolylinesBusy()) + return; + + var latLng = event.latlng; + if(that._options.newPolylineConfirmMessage) + if(!confirm(that._options.newPolylineConfirmMessage)) + return + + var contexts = [{'originalPolylineNo': null, 'originalPointNo': null}]; + L.Polyline.PolylineEditor([latLng], that._options, contexts).addTo(that._map); + + that._showBoundMarkers(); + that._changed = true; + }); + } + + /** + * Check if there is *any* busy editable polyline on this map. + */ + this._map.isEditablePolylinesBusy = function() { + var map = this; + for(var i = 0; i < map._editablePolylines.length; i++) + if(map._editablePolylines[i]._isBusy()) + return true; + + return false; + }; + + /** + * Enable/disable editing. + */ + this._map.setEditablePolylinesEnabled = function(enabled) { + var map = this; + map._editablePolylinesEnabled = enabled; + for(var i = 0; i < map._editablePolylines.length; i++) { + var polyline = map._editablePolylines[i]; + if(enabled) { + polyline._showBoundMarkers(); + } else { + polyline._hideAll(); + } + } + }; + + /* + * Utility method added to this map to retreive editable + * polylines. + */ + this._map.getEditablePolylines = function() { + var map = this; + return map._editablePolylines; + } + + this._map.fixAroundEditablePoint = function(marker) { + var map = this; + for(var i = 0; i < map._editablePolylines.length; i++) { + var polyline = map._editablePolylines[i]; + polyline._reloadPolyline(marker); + } + } + }, + /** + * Will add all needed methods to this polyline. + */ + _addMethods: function() { + var that = this; + + this._init = function(options, contexts) { + this._prepareMapIfNeeded(); + + /** + * Since all point editing is done by marker events, markers + * will be the main holder of the polyline points locations. + * Every marker contains a reference to the newPointMarker + * *before* him (=> the first marker has newPointMarker=null). + */ + this._parseOptions(options); + + this._markers = []; + var points = this.getLatLngs(); + var length = points.length; + for(var i = 0; i < length; i++) { + var marker = this._addMarkers(i, points[i]); + if(! ('context' in marker)) { + marker.context = {} + if(that._contexts != null) { + marker.context = contexts[i]; + } + } + + if(marker.context && ! ('originalPointNo' in marker.context)) + marker.context.originalPointNo = i; + if(marker.context && ! ('originalPolylineNo' in marker.context)) + marker.context.originalPolylineNo = that._map._editablePolylines.length; + } + + // Map move => show different editable markers: + var map = this._map; + this._map.on("zoomend", function(e) { + that._showBoundMarkers(); + }); + this._map.on("moveend", function(e) { + that._showBoundMarkers(); + }); + + if(this._desiredPolylineNo && this._desiredPolylineNo != null) { + this._map._editablePolylines.splice(this._desiredPolylineNo, 0, this); + } else { + this._map._editablePolylines.push(this); + } + }; + + /** + * Check if is busy adding/moving new nodes. Note, there may be + * *other* editable polylines on the same map which *are* busy. + */ + this._isBusy = function() { + return that._busy; + }; + + this._setBusy = function(busy) { + that._busy = busy; + }; + + /** + * Get markers for this polyline. + */ + this.getPoints = function() { + return this._markers; + }; + + this.isChanged = function() { + return this._changed; + } + + this._parseOptions = function(options) { + if(!options) + options = {}; + + // Do not show edit markers if more than maxMarkers would be shown: + if(!('maxMarkers' in options)) + options.maxMarkers = 100; + if(!('newPolylines' in options)) + options.newPolylines = false; + if(!('newPolylineConfirmMessage' in options)) + options.newPolylineConfirmMessage = ''; + if(!('addFirstLastPointEvent' in options)) + options.addFirstLastPointEvent = 'click'; + if(!('customPointListeners' in options)) + options.customPointListeners = {}; + if(!('customNewPointListeners' in options)) + options.customNewPointListeners = {}; + + this._options = options; + + // Icons: + if(!options.pointIcon) + this._options.pointIcon = L.icon({ iconUrl: 'editmarker.png', iconSize: [11, 11], iconAnchor: [6, 6] }); + if(!options.newPointIcon) + this._options.newPointIcon = L.icon({ iconUrl: 'editmarker2.png', iconSize: [11, 11], iconAnchor: [6, 6] }); + }; + + /** + * Show only markers in current map bounds *is* there are only a certain + * number of markers. This method is called on eventy that change map + * bounds. + */ + this._showBoundMarkers = function() { + if (!that._map) { + return; + } + + this._setBusy(false); + + if(!that._map._editablePolylinesEnabled) { + console.log('Do not show because editing is disabled'); + return; + } + + var bounds = that._map.getBounds(); + var found = 0; + for(var polylineNo in that._map._editablePolylines) { + var polyline = that._map._editablePolylines[polylineNo]; + for(var markerNo in polyline._markers) { + var marker = polyline._markers[markerNo]; + if(bounds.contains(marker.getLatLng())) + found += 1; + } + } + + for(var polylineNo in that._map._editablePolylines) { + var polyline = that._map._editablePolylines[polylineNo]; + for(var markerNo in polyline._markers) { + var marker = polyline._markers[markerNo]; + if(found < that._options.maxMarkers) { + that._setMarkerVisible(marker, bounds.contains(marker.getLatLng())); + that._setMarkerVisible(marker.newPointMarker, markerNo > 0 && bounds.contains(marker.getLatLng())); + } else { + that._setMarkerVisible(marker, false); + that._setMarkerVisible(marker.newPointMarker, false); + } + } + } + }; + + /** + * Used when adding/moving points in order to disable the user to mess + * with other markers (+ easier to decide where to put the point + * without too many markers). + */ + this._hideAll = function(except) { + this._setBusy(true); + for(var polylineNo in that._map._editablePolylines) { + console.log("hide " + polylineNo + " markers"); + var polyline = that._map._editablePolylines[polylineNo]; + for(var markerNo in polyline._markers) { + var marker = polyline._markers[markerNo]; + if(except == null || except != marker) + polyline._setMarkerVisible(marker, false); + if(except == null || except != marker.newPointMarker) + polyline._setMarkerVisible(marker.newPointMarker, false); + } + } + } + + /** + * Show/hide marker. + */ + this._setMarkerVisible = function(marker, show) { + if(!marker) + return; + + var map = this._map; + if(show) { + if(!marker._visible) { + if(!marker._map) { // First show for this marker: + marker.addTo(map); + } else { // Marker was already shown and hidden: + map.addLayer(marker); + } + marker._map = map; + } + marker._visible = true; + } else { + if(marker._visible) { + map.removeLayer(marker); + } + marker._visible = false; + } + }; + + /** + * Reload polyline. If it is busy, then the bound markers will not be + * shown. + */ + this._reloadPolyline = function(fixAroundPointNo) { + that.setLatLngs(that._getMarkerLatLngs()); + if(fixAroundPointNo != null) + that._fixAround(fixAroundPointNo); + that._showBoundMarkers(); + that._changed = true; + } + + /** + * Add two markers (a point marker and his newPointMarker) for a + * single point. + * + * Markers are not added on the map here, the marker.addTo(map) is called + * only later when needed first time because of performance issues. + */ + this._addMarkers = function(pointNo, latLng, fixNeighbourPositions) { + var that = this; + var points = this.getLatLngs(); + var marker = L.marker(latLng, {draggable: true, icon: this._options.pointIcon}); + + marker.newPointMarker = null; + marker.on('dragstart', function(event) { + var pointNo = that._getPointNo(event.target); + var previousPoint = pointNo && pointNo > 0 ? that._markers[pointNo - 1].getLatLng() : null; + var nextPoint = pointNo < that._markers.length - 1 ? that._markers[pointNo + 1].getLatLng() : null; + that._setupDragLines(marker, previousPoint, nextPoint); + that._hideAll(marker); + }); + marker.on('dragend', function(event) { + var marker = event.target; + var pointNo = that._getPointNo(event.target); + setTimeout(function() { + that._reloadPolyline(pointNo); + }, 25); + }); + marker.on('contextmenu', function(event) { + var marker = event.target; + var pointNo = that._getPointNo(event.target); + that._map.removeLayer(marker); + that._map.removeLayer(newPointMarker); + that._markers.splice(pointNo, 1); + that._reloadPolyline(pointNo); + }); + marker.on(that._options.addFirstLastPointEvent, function(event) { + + console.log('click on marker'); + var marker = event.target; + var pointNo = that._getPointNo(event.target); + console.log('pointNo=' + pointNo + ' that._markers.length=' + that._markers.length); + event.dont; + if(pointNo == 0 || pointNo == that._markers.length - 1) { + console.log('first or last'); + that._prepareForNewPoint(marker, pointNo == 0 ? 0 : pointNo + 1); + } else { + console.log('not first or last'); + } + }); + + var previousPoint = points[pointNo == 0 ? pointNo : pointNo - 1]; + var newPointMarker = L.marker([(latLng.lat + previousPoint.lat) / 2., + (latLng.lng + previousPoint.lng) / 2.], + {draggable: true, icon: this._options.newPointIcon}); + marker.newPointMarker = newPointMarker; + newPointMarker.on('dragstart', function(event) { + var pointNo = that._getPointNo(event.target); + var previousPoint = that._markers[pointNo - 1].getLatLng(); + var nextPoint = that._markers[pointNo].getLatLng(); + that._setupDragLines(marker.newPointMarker, previousPoint, nextPoint); + + that._hideAll(marker.newPointMarker); + }); + newPointMarker.on('dragend', function(event) { + var marker = event.target; + var pointNo = that._getPointNo(event.target); + that._addMarkers(pointNo, marker.getLatLng(), true); + setTimeout(function() { + that._reloadPolyline(); + }, 25); + }); + newPointMarker.on('contextmenu', function(event) { + // 1. Remove this polyline from map + var marker = event.target; + var pointNo = that._getPointNo(marker); + var markers = that.getPoints(); + that._hideAll(); + + var secondPartMarkers = that._markers.slice(pointNo, pointNo.length); + that._markers.splice(pointNo, that._markers.length - pointNo); + + that._reloadPolyline(); + + var points = []; + var contexts = []; + for(var i = 0; i < secondPartMarkers.length; i++) { + var marker = secondPartMarkers[i]; + points.push(marker.getLatLng()); + contexts.push(marker.context); + } + + console.log('points:' + points); + console.log('contexts:' + contexts); + + // Need to know the current polyline order numbers, because + // the splitted one need to be inserted immediately after: + var originalPolylineNo = that._map._editablePolylines.indexOf(that); + + L.Polyline.PolylineEditor(points, that._options, contexts, originalPolylineNo + 1) + .addTo(that._map); + + that._showBoundMarkers(); + }); + + this._markers.splice(pointNo, 0, marker); + + // User-defined custom event listeners: + if(that._options.customPointListeners) + for(var eventName in that._options.customPointListeners) + marker.on(eventName, that._options.customPointListeners[eventName]); + if(that._options.customNewPointListeners) + for(var eventName in that._options.customNewPointListeners) + newPointMarker.on(eventName, that._options.customNewPointListeners[eventName]); + + if(fixNeighbourPositions) { + this._fixAround(pointNo); + } + + return marker; + }; + + /** + * Event handlers for first and last point. + */ + this._prepareForNewPoint = function(marker, pointNo) { + // This is slightly delayed to prevent the same propagated event + // to be catched here: + setTimeout( + function() { + that._hideAll(); + that._setupDragLines(marker, marker.getLatLng()); + that._map.once('click', function(event) { + if(that._markers.length == 1) { + pointNo += 1; + } + console.log('dodajemo na ' + pointNo + ' - ' + event.latlng); + that._addMarkers(pointNo, event.latlng, true); + that._reloadPolyline(); + }); + }, + 100 + ); + }; + + /** + * Fix nearby new point markers when the new point is created. + */ + this._fixAround = function(pointNoOrMarker) { + if((typeof pointNoOrMarker) == 'number') + var pointNo = pointNoOrMarker; + else + var pointNo = that._markers.indexOf(pointNoOrMarker); + + if(pointNo < 0) + return; + + var previousMarker = pointNo == 0 ? null : that._markers[pointNo - 1]; + var marker = that._markers[pointNo]; + var nextMarker = pointNo < that._markers.length - 1 ? that._markers[pointNo + 1] : null; + if(marker && previousMarker) { + marker.newPointMarker.setLatLng([(previousMarker.getLatLng().lat + marker.getLatLng().lat) / 2., + (previousMarker.getLatLng().lng + marker.getLatLng().lng) / 2.]); + } + if(marker && nextMarker) { + nextMarker.newPointMarker.setLatLng([(marker.getLatLng().lat + nextMarker.getLatLng().lat) / 2., + (marker.getLatLng().lng + nextMarker.getLatLng().lng) / 2.]); + } + }; + + /** + * Find the order number of the marker. + */ + this._getPointNo = function(marker) { + for(var i = 0; i < this._markers.length; i++) { + if(marker == this._markers[i] || marker == this._markers[i].newPointMarker) { + return i; + } + } + return -1; + }; + + /** + * Get polyline latLngs based on marker positions. + */ + this._getMarkerLatLngs = function() { + var result = []; + for(var i = 0; i < this._markers.length; i++) + result.push(this._markers[i].getLatLng()); + return result; + }; + + this._setupDragLines = function(marker, point1, point2) { + var line1 = null; + var line2 = null; + if(point1) line1 = L.polyline([marker.getLatLng(), point1], {dasharray: "5,1", weight: 1}) + .addTo(that._map); + if(point2) line2 = L.polyline([marker.getLatLng(), point2], {dasharray: "5,1", weight: 1}) + .addTo(that._map); + + var moveHandler = function(event) { + if(line1) + line1.setLatLngs([event.latlng, point1]); + if(line2) + line2.setLatLngs([event.latlng, point2]); + }; + + var stopHandler = function(event) { + if (that._map) { + that._map.off('mousemove', moveHandler); + marker.off('dragend', stopHandler); + if(line1) that._map.removeLayer(line1); + if(line2) that._map.removeLayer(line2); + console.log('STOPPED'); + if(event.target != that._map) { + that._map.fire('click', event); + } + } + }; + + that._map.on('mousemove', moveHandler); + marker.on('dragend', stopHandler); + + that._map.once('click', stopHandler); + marker.once('click', stopHandler); + if(line1) line1.once('click', stopHandler); + if(line2) line2.once('click', stopHandler); + } + } +}); + +L.Polyline.polylineEditor.addInitHook(function () { + this.on('add', function(event) { + this._map = event.target._map; + this._addMethods(); + + /** + * When addint a new point we must disable the user to mess with other + * markers. One way is to check everywhere if the user is busy. The + * other is to just remove other markers when the user is doing + * somethinng. + * + * TODO: Decide the right way to do this and then leave only _busy or + * _hideAll(). + */ + this._busy = false; + this._initialized = false; + + this._init(this._options, this._contexts); + + this._initialized = true; + + return this; + }); + + this.on('remove', function(event) { + var polyline = event.target; + var map = polyline._map; + var polylines = map.getEditablePolylines(); + var index = polylines.indexOf(polyline); + if (index > -1) { + polylines[index]._markers.forEach(function(marker) { + map.removeLayer(marker); + if(marker.newPointMarker) + map.removeLayer(marker.newPointMarker); + }); + polylines.splice(index, 1); + } + }); +}); + +/** + * Construct a new editable polyline. + * + * latlngs ... a list of points (or two-element tuples with coordinates) + * options ... polyline options + * contexts ... custom contexts for every point in the polyline. Must have the + * same number of elements as latlngs and this data will be + * preserved when new points are added or polylines splitted. + * polylineNo ... insert this polyline in a specific order (used when splitting). + * + * More about contexts: + * This is an array of objects that will be kept as "context" for every + * point. Marker will keep this value as marker.context. New markers will + * have context set to null. + * + * Contexts must be the same size as the polyline size! + * + * By default, even without calling this method -- every marker will have + * context with one value: marker.context.originalPointNo with the + * original order number of this point. The order may change if some + * markers before this one are delted or new added. + */ +L.Polyline.PolylineEditor = function(latlngs, options, contexts, polylineNo) { + // Since the app code may not be able to explicitly call the + // initialization of all editable polylines (if the user created a new + // one by splitting an existing), with this method you can control the + // options for new polylines: + if(options.prepareOptions) { + options.prepareOptions(options); + } + + var result = new L.Polyline.polylineEditor(latlngs, options); + result._options = options; + result._contexts = contexts; + result._desiredPolylineNo = polylineNo + + return result; +}; diff --git a/tools/createtiles.sh b/tools/createtiles.sh new file mode 100755 index 0000000..0ea34c9 --- /dev/null +++ b/tools/createtiles.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +# do NOT forget to install `python-gdal` library +# assuming you are on a debian like OS +#sudo apt install python-gdal + +# get the tool +test ! -f gdal2tiles.py \ + && curl https://raw.githubusercontent.com/commenthol/gdal2tiles-leaflet/master/gdal2tiles.py \ + > gdal2tiles.py \ + && echo "'python-gdal' library required - please install" + +# process ... +export GDAL_ALLOW_LARGE_LIBJPEG_MEM_ALLOC=1 +python3 ./gdal2tiles.py -l -p raster -z 0-4 -w none 20230330_143536.jpg tiles + +echo 'Now open "index.html" in your browser.' diff --git a/watermark.js b/watermark.js new file mode 100644 index 0000000..f2007ad --- /dev/null +++ b/watermark.js @@ -0,0 +1,14 @@ +var css = "font-family: monospace; text-color: red;"; + console.log(`%c + TTTTTTT + T + T GGGG + G + G GGG + G DDDD + GGGGG D + D D + DDDD +Whats on my Laptop? +© Sören Oesterwind aka thegreydiamond.de +`, css); \ No newline at end of file