OSDN Git Service

044b2e10e59c845940835e65820b5fc6c0cdee0c
[opentween/open-tween.git] / OpenTween / Thumbnail / MapThumbOSM.cs
1 // OpenTween - Client of Twitter
2 // Copyright (c) 2012 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
3 // All rights reserved.
4 //
5 // This file is part of OpenTween.
6 //
7 // This program is free software; you can redistribute it and/or modify it
8 // under the terms of the GNU General Public License as published by the Free
9 // Software Foundation; either version 3 of the License, or (at your option)
10 // any later version.
11 //
12 // This program is distributed in the hope that it will be useful, but
13 // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
15 // for more details.
16 //
17 // You should have received a copy of the GNU General Public License along
18 // with this program. If not, see <http://www.gnu.org/licenses/>, or write to
19 // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
20 // Boston, MA 02110-1301, USA.
21
22 using System;
23 using System.Collections.Generic;
24 using System.Drawing;
25 using System.Linq;
26 using System.Net.Http;
27 using System.Text;
28 using System.Threading;
29 using System.Threading.Tasks;
30 using OpenTween.Models;
31 using OpenTween.Setting;
32
33 namespace OpenTween.Thumbnail
34 {
35     class MapThumbOSM : MapThumb
36     {
37         public override Task<ThumbnailInfo> GetThumbnailInfoAsync(PostClass.StatusGeo geo)
38         {
39             var size = new Size(SettingManager.Common.MapThumbnailWidth, SettingManager.Common.MapThumbnailHeight);
40             var zoom = SettingManager.Common.MapThumbnailZoom;
41
42             var thumb = new OSMThumbnailInfo(geo.Latitude, geo.Longitude, zoom, size)
43             {
44                 MediaPageUrl = this.CreateMapLinkUrl(geo.Latitude, geo.Longitude),
45             };
46
47             return Task.FromResult((ThumbnailInfo)thumb);
48         }
49
50         public string CreateMapLinkUrl(double latitude, double longitude)
51         {
52             var zoom = SettingManager.Common.MapThumbnailZoom;
53
54             return $"https://www.openstreetmap.org/?mlat={latitude}&mlon={longitude}#map={zoom}/{latitude}/{longitude}";
55         }
56     }
57
58     public class OSMThumbnailInfo : ThumbnailInfo
59     {
60         /// <summary>openstreetmap.org タイルサーバー</summary>
61         public static readonly string TileServerBase = "https://a.tile.openstreetmap.org";
62
63         /// <summary>タイル画像一枚当たりの大きさ (ピクセル単位)</summary>
64         public static readonly Size TileSize = new Size(256, 256);
65
66         /// <summary>画像の中心点の緯度</summary>
67         public double Latitude { get; }
68
69         /// <summary>画像の中心点の経度</summary>
70         public double Longitude { get; }
71
72         /// <summary>地図のズームレベル</summary>
73         public int Zoom { get; }
74
75         /// <summary>生成するサムネイル画像のサイズ (ピクセル単位)</summary>
76         public Size ThumbnailSize { get; }
77
78         public OSMThumbnailInfo(double latitude, double longitude, int zoom, Size thumbSize)
79         {
80             this.Latitude = latitude;
81             this.Longitude = longitude;
82             this.Zoom = zoom;
83             this.ThumbnailSize = thumbSize;
84         }
85
86         public override async Task<MemoryImage> LoadThumbnailImageAsync(HttpClient http, CancellationToken cancellationToken)
87         {
88             // 画像中央に描画されるタイル (ピクセル単位ではなくタイル番号を表す)
89             // タイル番号に小数部が含まれているが、これはタイル内の相対的な位置を表すためこのまま保持する
90             var centerTileNum = this.WorldToTilePos(this.Longitude, this.Latitude, this.Zoom);
91
92             // 画像左上に描画されるタイル
93             var topLeftTileNum = PointF.Add(centerTileNum, new SizeF(-this.ThumbnailSize.Width / 2.0f / TileSize.Width, -this.ThumbnailSize.Height / 2.0f / TileSize.Height));
94
95             // タイル番号の小数部をもとに、タイル画像を描画する際のピクセル単位のオフセットを算出する
96             var tileOffset = Size.Round(new SizeF(-TileSize.Width * (topLeftTileNum.X - (int)topLeftTileNum.X), -TileSize.Height * (topLeftTileNum.Y - (int)topLeftTileNum.Y)));
97
98             // 縦横のタイル枚数
99             var tileCountX = (int)Math.Ceiling((double)(this.ThumbnailSize.Width + Math.Abs(tileOffset.Width)) / TileSize.Width);
100             var tileCountY = (int)Math.Ceiling((double)(this.ThumbnailSize.Height + Math.Abs(tileOffset.Height)) / TileSize.Height);
101
102             // 読み込む対象となるタイル画像が 10 枚を越えていたら中断
103             // ex. 一辺が 512px 以内のサムネイル画像を生成する場合、必要なタイル画像は最大で 9 枚
104             if (tileCountX * tileCountY > 10)
105                 throw new OperationCanceledException();
106
107             // タイル画像を読み込む
108             var tilesTask = new Task<MemoryImage>[tileCountX, tileCountY];
109
110             foreach (var x in Enumerable.Range(0, tileCountX))
111             {
112                 foreach (var y in Enumerable.Range(0, tileCountY))
113                 {
114                     var tilePos = Point.Add(Point.Truncate(topLeftTileNum), new Size(x, y));
115                     tilesTask[x, y] = this.LoadTileImageAsync(http, tilePos);
116                 }
117             }
118
119             await Task.WhenAll(tilesTask.Cast<Task<MemoryImage>>())
120                 .ConfigureAwait(false);
121
122             using var bitmap = new Bitmap(this.ThumbnailSize.Width, this.ThumbnailSize.Height);
123
124             using (var g = Graphics.FromImage(bitmap))
125             {
126                 g.TranslateTransform(tileOffset.Width, tileOffset.Height);
127
128                 foreach (var x in Enumerable.Range(0, tileCountX))
129                 {
130                     foreach (var y in Enumerable.Range(0, tileCountY))
131                     {
132                         using var image = tilesTask[x, y].Result;
133                         g.DrawImage(image.Image, TileSize.Width * x, TileSize.Height * y);
134                     }
135                 }
136             }
137
138             MemoryImage result = null;
139             try
140             {
141                 result = MemoryImage.CopyFromImage(bitmap);
142                 return result;
143             }
144             catch { result?.Dispose(); throw; }
145         }
146
147         /// <summary>指定されたタイル番号のタイル画像を読み込むメソッド</summary>
148         private async Task<MemoryImage> LoadTileImageAsync(HttpClient http, Point pos)
149         {
150             var tileUrl = TileServerBase + $"/{this.Zoom}/{pos.X}/{pos.Y}.png";
151
152             using var stream = await http.GetStreamAsync(tileUrl)
153                 .ConfigureAwait(false);
154
155             MemoryImage result = null;
156             try
157             {
158                 result = await MemoryImage.CopyFromStreamAsync(stream).ConfigureAwait(false);
159                 return result;
160             }
161             catch { result?.Dispose(); throw; }
162         }
163
164         /// <summary>経度・緯度からタイル番号を算出するメソッド</summary>
165         private PointF WorldToTilePos(double lon, double lat, int zoom)
166         {
167             // 計算式は http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#C.23 に基づく
168             return new PointF
169             {
170                 X = (float)((lon + 180.0) / 360.0 * (1 << zoom)),
171                 Y = (float)((1.0 - Math.Log(Math.Tan(lat * Math.PI / 180.0) +
172                     1.0 / Math.Cos(lat * Math.PI / 180.0)) / Math.PI) / 2.0 * (1 << zoom)),
173             };
174         }
175     }
176 }