1 // OpenTween - Client of Twitter
2 // Copyright (c) 2012 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
3 // All rights reserved.
5 // This file is part of OpenTween.
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)
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
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.
23 using System.Collections.Generic;
26 using System.Net.Http;
28 using System.Threading;
29 using System.Threading.Tasks;
30 using OpenTween.Models;
31 using OpenTween.Setting;
33 namespace OpenTween.Thumbnail
35 class MapThumbOSM : MapThumb
37 public override Task<ThumbnailInfo> GetThumbnailInfoAsync(PostClass.StatusGeo geo)
39 var size = new Size(SettingManager.Common.MapThumbnailWidth, SettingManager.Common.MapThumbnailHeight);
40 var zoom = SettingManager.Common.MapThumbnailZoom;
42 var thumb = new OSMThumbnailInfo(geo.Latitude, geo.Longitude, zoom, size)
44 MediaPageUrl = this.CreateMapLinkUrl(geo.Latitude, geo.Longitude),
47 return Task.FromResult((ThumbnailInfo)thumb);
50 public string CreateMapLinkUrl(double latitude, double longitude)
52 var zoom = SettingManager.Common.MapThumbnailZoom;
54 return $"https://www.openstreetmap.org/?mlat={latitude}&mlon={longitude}#map={zoom}/{latitude}/{longitude}";
58 public class OSMThumbnailInfo : ThumbnailInfo
60 /// <summary>openstreetmap.org タイルサーバー</summary>
61 public static readonly string TileServerBase = "https://a.tile.openstreetmap.org";
63 /// <summary>タイル画像一枚当たりの大きさ (ピクセル単位)</summary>
64 public static readonly Size TileSize = new Size(256, 256);
66 /// <summary>画像の中心点の緯度</summary>
67 public double Latitude { get; }
69 /// <summary>画像の中心点の経度</summary>
70 public double Longitude { get; }
72 /// <summary>地図のズームレベル</summary>
73 public int Zoom { get; }
75 /// <summary>生成するサムネイル画像のサイズ (ピクセル単位)</summary>
76 public Size ThumbnailSize { get; }
78 public OSMThumbnailInfo(double latitude, double longitude, int zoom, Size thumbSize)
80 this.Latitude = latitude;
81 this.Longitude = longitude;
83 this.ThumbnailSize = thumbSize;
86 public override async Task<MemoryImage> LoadThumbnailImageAsync(HttpClient http, CancellationToken cancellationToken)
88 // 画像中央に描画されるタイル (ピクセル単位ではなくタイル番号を表す)
89 // タイル番号に小数部が含まれているが、これはタイル内の相対的な位置を表すためこのまま保持する
90 var centerTileNum = this.WorldToTilePos(this.Longitude, this.Latitude, this.Zoom);
93 var topLeftTileNum = PointF.Add(centerTileNum, new SizeF(-this.ThumbnailSize.Width / 2.0f / TileSize.Width, -this.ThumbnailSize.Height / 2.0f / TileSize.Height));
95 // タイル番号の小数部をもとに、タイル画像を描画する際のピクセル単位のオフセットを算出する
96 var tileOffset = Size.Round(new SizeF(-TileSize.Width * (topLeftTileNum.X - (int)topLeftTileNum.X), -TileSize.Height * (topLeftTileNum.Y - (int)topLeftTileNum.Y)));
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);
102 // 読み込む対象となるタイル画像が 10 枚を越えていたら中断
103 // ex. 一辺が 512px 以内のサムネイル画像を生成する場合、必要なタイル画像は最大で 9 枚
104 if (tileCountX * tileCountY > 10)
105 throw new OperationCanceledException();
108 var tilesTask = new Task<MemoryImage>[tileCountX, tileCountY];
110 foreach (var x in Enumerable.Range(0, tileCountX))
112 foreach (var y in Enumerable.Range(0, tileCountY))
114 var tilePos = Point.Add(Point.Truncate(topLeftTileNum), new Size(x, y));
115 tilesTask[x, y] = this.LoadTileImageAsync(http, tilePos);
119 await Task.WhenAll(tilesTask.Cast<Task<MemoryImage>>())
120 .ConfigureAwait(false);
122 using var bitmap = new Bitmap(this.ThumbnailSize.Width, this.ThumbnailSize.Height);
124 using (var g = Graphics.FromImage(bitmap))
126 g.TranslateTransform(tileOffset.Width, tileOffset.Height);
128 foreach (var x in Enumerable.Range(0, tileCountX))
130 foreach (var y in Enumerable.Range(0, tileCountY))
132 using var image = tilesTask[x, y].Result;
133 g.DrawImage(image.Image, TileSize.Width * x, TileSize.Height * y);
138 MemoryImage result = null;
141 result = MemoryImage.CopyFromImage(bitmap);
144 catch { result?.Dispose(); throw; }
147 /// <summary>指定されたタイル番号のタイル画像を読み込むメソッド</summary>
148 private async Task<MemoryImage> LoadTileImageAsync(HttpClient http, Point pos)
150 var tileUrl = TileServerBase + $"/{this.Zoom}/{pos.X}/{pos.Y}.png";
152 using var stream = await http.GetStreamAsync(tileUrl)
153 .ConfigureAwait(false);
155 MemoryImage result = null;
158 result = await MemoryImage.CopyFromStreamAsync(stream).ConfigureAwait(false);
161 catch { result?.Dispose(); throw; }
164 /// <summary>経度・緯度からタイル番号を算出するメソッド</summary>
165 private PointF WorldToTilePos(double lon, double lat, int zoom)
167 // 計算式は http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#C.23 に基づく
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)),