// OpenTween - Client of Twitter
// Copyright (c) 2012 kim_upsilon (@kim_upsilon)
// All rights reserved.
//
// This file is part of OpenTween.
//
// This program is free software; you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation; either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
// for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program. If not, see , or write to
// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
// Boston, MA 02110-1301, USA.
#nullable enable
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using OpenTween.Models;
using OpenTween.Setting;
namespace OpenTween.Thumbnail
{
public class MapThumbOSM : MapThumb
{
public override Task GetThumbnailInfoAsync(PostClass.StatusGeo geo)
{
var size = new Size(SettingManager.Instance.Common.MapThumbnailWidth, SettingManager.Instance.Common.MapThumbnailHeight);
var zoom = SettingManager.Instance.Common.MapThumbnailZoom;
var thumb = new OSMThumbnailInfo(geo.Latitude, geo.Longitude, zoom, size)
{
MediaPageUrl = this.CreateMapLinkUrl(geo.Latitude, geo.Longitude),
};
return Task.FromResult((ThumbnailInfo)thumb);
}
public string CreateMapLinkUrl(double latitude, double longitude)
{
var zoom = SettingManager.Instance.Common.MapThumbnailZoom;
return $"https://www.openstreetmap.org/?mlat={latitude}&mlon={longitude}#map={zoom}/{latitude}/{longitude}";
}
}
public class OSMThumbnailInfo : ThumbnailInfo
{
/// openstreetmap.org タイルサーバー
public static readonly string TileServerBase = "https://a.tile.openstreetmap.org";
/// タイル画像一枚当たりの大きさ (ピクセル単位)
public static readonly Size TileSize = new(256, 256);
/// 画像の中心点の緯度
public double Latitude { get; }
/// 画像の中心点の経度
public double Longitude { get; }
/// 地図のズームレベル
public int Zoom { get; }
/// 生成するサムネイル画像のサイズ (ピクセル単位)
public Size ThumbnailSize { get; }
public OSMThumbnailInfo(double latitude, double longitude, int zoom, Size thumbSize)
{
this.Latitude = latitude;
this.Longitude = longitude;
this.Zoom = zoom;
this.ThumbnailSize = thumbSize;
}
public override async Task LoadThumbnailImageAsync(HttpClient http, CancellationToken cancellationToken)
{
// 画像中央に描画されるタイル (ピクセル単位ではなくタイル番号を表す)
// タイル番号に小数部が含まれているが、これはタイル内の相対的な位置を表すためこのまま保持する
var centerTileNum = this.WorldToTilePos(this.Longitude, this.Latitude, this.Zoom);
// 画像左上に描画されるタイル
var topLeftTileNum = PointF.Add(centerTileNum, new SizeF(-this.ThumbnailSize.Width / 2.0f / TileSize.Width, -this.ThumbnailSize.Height / 2.0f / TileSize.Height));
// タイル番号の小数部をもとに、タイル画像を描画する際のピクセル単位のオフセットを算出する
var tileOffset = Size.Round(new SizeF(-TileSize.Width * (topLeftTileNum.X - (int)topLeftTileNum.X), -TileSize.Height * (topLeftTileNum.Y - (int)topLeftTileNum.Y)));
// 縦横のタイル枚数
var tileCountX = (int)Math.Ceiling((double)(this.ThumbnailSize.Width + Math.Abs(tileOffset.Width)) / TileSize.Width);
var tileCountY = (int)Math.Ceiling((double)(this.ThumbnailSize.Height + Math.Abs(tileOffset.Height)) / TileSize.Height);
// 読み込む対象となるタイル画像が 10 枚を越えていたら中断
// ex. 一辺が 512px 以内のサムネイル画像を生成する場合、必要なタイル画像は最大で 9 枚
if (tileCountX * tileCountY > 10)
throw new OperationCanceledException();
// タイル画像を読み込む
var tilesTask = new Task[tileCountX, tileCountY];
foreach (var x in Enumerable.Range(0, tileCountX))
{
foreach (var y in Enumerable.Range(0, tileCountY))
{
var tilePos = Point.Add(Point.Truncate(topLeftTileNum), new Size(x, y));
tilesTask[x, y] = this.LoadTileImageAsync(http, tilePos);
}
}
await Task.WhenAll(tilesTask.Cast>())
.ConfigureAwait(false);
using var bitmap = new Bitmap(this.ThumbnailSize.Width, this.ThumbnailSize.Height);
using (var g = Graphics.FromImage(bitmap))
{
g.TranslateTransform(tileOffset.Width, tileOffset.Height);
foreach (var x in Enumerable.Range(0, tileCountX))
{
foreach (var y in Enumerable.Range(0, tileCountY))
{
using var image = tilesTask[x, y].Result;
g.DrawImage(image.Image, TileSize.Width * x, TileSize.Height * y);
}
}
}
MemoryImage? result = null;
try
{
result = MemoryImage.CopyFromImage(bitmap);
return result;
}
catch
{
result?.Dispose();
throw;
}
}
/// 指定されたタイル番号のタイル画像を読み込むメソッド
private async Task LoadTileImageAsync(HttpClient http, Point pos)
{
var tileUrl = TileServerBase + $"/{this.Zoom}/{pos.X}/{pos.Y}.png";
using var stream = await http.GetStreamAsync(tileUrl)
.ConfigureAwait(false);
MemoryImage? result = null;
try
{
result = await MemoryImage.CopyFromStreamAsync(stream).ConfigureAwait(false);
return result;
}
catch
{
result?.Dispose();
throw;
}
}
/// 経度・緯度からタイル番号を算出するメソッド
private PointF WorldToTilePos(double lon, double lat, int zoom)
{
// 計算式は http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#C.23 に基づく
return new PointF
{
X = (float)((lon + 180.0) / 360.0 * (1 << zoom)),
Y = (float)((1.0 - Math.Log(Math.Tan(lat * Math.PI / 180.0) +
1.0 / Math.Cos(lat * Math.PI / 180.0)) / Math.PI) / 2.0 * (1 << zoom)),
};
}
}
}