private void DrawWaterMark(ImageCreationContext context) { if (context.Width < 200) { // too small... return; } var text = "IsraelHikingMap.osm.org.il"; var initialFontSize = 16; var fontFamiliyName = "Arial"; using (Graphics graphics = Graphics.FromImage(context.Image)) using (var fill = new SolidBrush(Color.FromArgb(128, 0, 0, 0))) using (var outline = new Pen(Color.FromArgb(128, 255, 255, 255), 6)) { Font stringFont = new Font(fontFamiliyName, initialFontSize); var stringSize = graphics.MeasureString(text, stringFont); var ratio = (context.Width - 4) / stringSize.Width; var bottomPosition = context.Height - 2 - stringSize.Height * ratio; var fontSize = initialFontSize * ratio; stringFont = new Font(fontFamiliyName, fontSize); // Draw string to screen. GraphicsPath path = new GraphicsPath(); path.AddString(text, new FontFamily(fontFamiliyName), (int)FontStyle.Regular, graphics.DpiY * fontSize / 72, // em size, new PointF(2, bottomPosition), new StringFormat()); graphics.DrawPath(outline, path); graphics.FillPath(fill, path); } }
/// <summary> /// Allows conversion between the image pixels and wgs84 coordinates /// </summary> /// <param name="latLng"></param> /// <param name="context"></param> /// <returns></returns> private PointF ConvertLatLngToPoint(LatLng latLng, ImageCreationContext context) { var x = (float)((GetXTile(latLng.Lng, context.N) - context.TopLeft.X) * TILE_SIZE); var y = (float)((GetYTile(latLng.Lat, context.N) - context.TopLeft.Y) * TILE_SIZE); return(new PointF(x, y)); }
/// <summary> /// Will create a single image from all the tiles - this image will include the required image inside /// </summary> /// <param name="context"></param> /// <returns></returns> private async Task <Image> CreateSingleImageFromTiles(ImageCreationContext context) { var horizontalTiles = context.BottomRight.X - context.TopLeft.X + 1; var verticalTiles = context.BottomRight.Y - context.TopLeft.Y + 1; var tasks = new List <Task <ImageWithOffset> >(); foreach (var addressTemplate in context.AddressesTemplates) { for (int x = 0; x < horizontalTiles; x++) { for (int y = 0; y < verticalTiles; y++) { var task = GetTileImage(context.TopLeft, new Point(x, y), context.Zoom, addressTemplate); tasks.Add(task); } } } var imagesWithOffsets = await Task.WhenAll(tasks); var image = new Image <Rgba32>(horizontalTiles * TILE_SIZE, verticalTiles * TILE_SIZE); foreach (var imageWithOffset in imagesWithOffsets) { image.Mutate(x => x.DrawImage(imageWithOffset.Image, new Point(imageWithOffset.Offset.X * TILE_SIZE, imageWithOffset.Offset.Y * TILE_SIZE), 1.0f)); } return(image); }
/// <summary> /// This will update the address templates and remove empty addresses /// </summary> /// <param name="context"></param> private void UpdateAddressesTemplates(ImageCreationContext context) { var address = IsValidAddress(context.DataContainer.BaseLayer.Address) ? context.DataContainer.BaseLayer.Address : GetBaseAddressFromInvalidString(context.DataContainer.BaseLayer.Address); var addressTemplates = new List <AddressAndOpacity> { new AddressAndOpacity { Address = FixAdrressTemplate(address), Opacity = context.DataContainer.BaseLayer.Opacity ?? 1.0 } }; foreach (var layerData in context.DataContainer.Overlays ?? new List <LayerData>()) { if (IsValidAddress(layerData.Address) == false) { continue; } var addressAndOpacity = new AddressAndOpacity { Address = FixAdrressTemplate(layerData.Address), Opacity = layerData.Opacity ?? 1.0 }; addressTemplates.Add(addressAndOpacity); } context.AddressesTemplates = addressTemplates.ToArray(); }
/// <summary> /// Will create a single image from all the tiles - this image will include the required image inside /// </summary> /// <param name="context"></param> /// <returns></returns> private async Task <Image> CreateSingleImageFromTiles(ImageCreationContext context) { var horizontalTiles = context.BottomRight.X - context.TopLeft.X + 1; var verticalTiles = context.BottomRight.Y - context.TopLeft.Y + 1; var bitmap = new Bitmap(horizontalTiles * TILE_SIZE, verticalTiles * TILE_SIZE); var tasks = new List <Task <ImageWithOffset> >(); foreach (var addressTemplate in context.AddressesTemplates) { for (int x = 0; x < horizontalTiles; x++) { for (int y = 0; y < verticalTiles; y++) { var task = GetTileImage(context.TopLeft, new Point(x, y), context.Zoom, addressTemplate); tasks.Add(task); } } } var imagesWithOffsets = await Task.WhenAll(tasks); using (var graphics = Graphics.FromImage(bitmap)) { foreach (var imageWithOffset in imagesWithOffsets) { graphics.DrawImage(imageWithOffset.Image, new Rectangle(imageWithOffset.Offset.X * TILE_SIZE, imageWithOffset.Offset.Y * TILE_SIZE, TILE_SIZE, TILE_SIZE), new Rectangle(0, 0, imageWithOffset.Image.Width, imageWithOffset.Image.Height), GraphicsUnit.Pixel); } } return(bitmap); }
/// <summary> /// This will update the address templates and remove empty addresses /// </summary> /// <param name="context"></param> private void UpdateAddressesTemplates(ImageCreationContext context) { var address = IsValidAddress(context.DataContainer.BaseLayer.Address) == false ? "https://israelhiking.osm.org.il/Hebrew/tiles/{z}/{x}/{y}.png" : context.DataContainer.BaseLayer.Address; var addressTemplates = new List <AddressAndOpacity> { new AddressAndOpacity { Address = FixAdrressTemplate(address), Opacity = context.DataContainer.BaseLayer.Opacity ?? 1.0 } }; foreach (var layerData in context.DataContainer.Overlays ?? new List <LayerData>()) { if (IsValidAddress(layerData.Address) == false) { continue; } var addressAndOpacity = new AddressAndOpacity { Address = FixAdrressTemplate(layerData.Address), Opacity = layerData.Opacity ?? 1.0 }; addressTemplates.Add(addressAndOpacity); } context.AddressesTemplates = addressTemplates.ToArray(); }
/// <summary> /// This will update the backgroud image - which is the image created from the baselayer and the overlay tiles /// </summary> /// <param name="context"></param> /// <returns></returns> private async Task UpdateBackGroundImage(ImageCreationContext context) { var topLeft = new Point((int)GetXTile(context.DataContainer.SouthWest.Lng, context.N), (int)GetYTile(context.DataContainer.NorthEast.Lat, context.N)); var bottomRight = new Point((int)GetXTile(context.DataContainer.NorthEast.Lng, context.N), (int)GetYTile(context.DataContainer.SouthWest.Lat, context.N)); context.TopLeft = topLeft; context.BottomRight = bottomRight; context.Image = await CreateSingleImageFromTiles(context); }
/// <summary> /// Crop and resizes the image to the desired dimentions /// </summary> /// <param name="context"></param> private void CropAndResizeImage(ImageCreationContext context) { var topLeft = ConvertLatLngToPoint(context.DataContainer.SouthWest, context); var bottomRight = ConvertLatLngToPoint(context.DataContainer.NorthEast, context); context.Image.Mutate(x => x.Crop(new Rectangle((int)topLeft.X, (int)bottomRight.Y, (int)(bottomRight.X - topLeft.X), (int)(topLeft.Y - bottomRight.Y))) .Resize(context.Width, context.Height) ); }
/// <summary> /// Crop and resizes the image to the desired dimentions /// </summary> /// <param name="context"></param> private void CropAndResizeImage(ImageCreationContext context) { Bitmap bmp = new Bitmap(context.Width, context.Height); using (Graphics graphics = Graphics.FromImage(bmp)) { var topLeft = ConvertLatLngToPoint(context.DataContainer.SouthWest, context); var bottomRight = ConvertLatLngToPoint(context.DataContainer.NorthEast, context); graphics.DrawImage(context.Image, new Rectangle(0, 0, bmp.Width, bmp.Height), topLeft.X, bottomRight.Y, bottomRight.X - topLeft.X, topLeft.Y - bottomRight.Y, GraphicsUnit.Pixel); } context.Image = bmp; }
/// <summary> /// This will draw on the backgroup image the route and markers according to color and opacity /// </summary> /// <param name="context"></param> private void DrawRoutesOnImage(ImageCreationContext context) { var penWidth = PEN_WIDTH; var penWidthOffset = 8; var circleOutlineWidth = 7f; var circleSize = CIRCLE_SIZE; using (var graphics = Graphics.FromImage(context.Image)) using (var outLinerPen = new Pen(Color.White, penWidth + penWidthOffset) { LineJoin = LineJoin.Bevel }) using (var circleFillBrush = new SolidBrush(Color.White)) using (var startRoutePen = new Pen(Color.Green, circleOutlineWidth)) using (var endRoutePen = new Pen(Color.Red, circleOutlineWidth)) { var routeColorIndex = 0; foreach (var route in context.DataContainer.Routes) { var points = route.Segments.SelectMany(s => s.Latlngs).Select(l => ConvertLatLngToPoint(l, context)).ToArray(); var markerPoints = route.Markers.Select(m => ConvertLatLngToPoint(m.Latlng, context)); var lineColor = _routeColors[routeColorIndex++]; routeColorIndex = routeColorIndex % _routeColors.Length; if (!string.IsNullOrEmpty(route.Color)) { lineColor = FromColorString(route.Color, route.Opacity); } using (var linePen = new Pen(lineColor, penWidth) { LineJoin = LineJoin.Bevel }) { if (points.Any()) { graphics.DrawLines(outLinerPen, points); graphics.DrawLines(linePen, points); graphics.FillEllipse(circleFillBrush, points.First().X - circleSize / 2, points.First().Y - circleSize / 2, circleSize, circleSize); graphics.DrawEllipse(startRoutePen, points.First().X - circleSize / 2, points.First().Y - circleSize / 2, circleSize, circleSize); graphics.FillEllipse(circleFillBrush, points.Last().X - circleSize / 2, points.Last().Y - circleSize / 2, circleSize, circleSize); graphics.DrawEllipse(endRoutePen, points.Last().X - circleSize / 2, points.Last().Y - circleSize / 2, circleSize, circleSize); } foreach (var markerPoint in markerPoints) { graphics.FillEllipse(circleFillBrush, markerPoint.X - circleSize / 2, markerPoint.Y - circleSize / 2, circleSize, circleSize); graphics.DrawEllipse(linePen, markerPoint.X - circleSize / 2, markerPoint.Y - circleSize / 2, circleSize, circleSize); } } } } }
/// <summary> /// Updates the zoom that will have tiles that have more pixels than the image needs /// This will make sure the image size will be bigger than the desired image to improve quility /// </summary> /// <param name="context"></param> private void UpdateZoom(ImageCreationContext context) { var zoom = 0; double n; double deltaX; double deltaY; do { zoom++; n = Math.Pow(2, zoom); deltaX = GetXTile(context.DataContainer.NorthEast.Lng, n) - GetXTile(context.DataContainer.SouthWest.Lng, n); deltaY = GetYTile(context.DataContainer.SouthWest.Lat, n) - GetYTile(context.DataContainer.NorthEast.Lat, n); deltaX *= TILE_SIZE; deltaY *= TILE_SIZE; } while (deltaX < context.Width && deltaY < context.Height); context.Zoom = zoom; context.N = n; }
/// <summary> /// Updates the conrners of the datacontainer to fix the relevant image ratio /// It will increase the height or width of the container as needed. /// </summary> /// <param name="context"></param> private void UpdateCorners(ImageCreationContext context) { if (context.DataContainer.NorthEast == null || context.DataContainer.SouthWest == null) { var allLocations = context.DataContainer.Routes .SelectMany(r => r.Segments) .SelectMany(s => s.Latlngs) .Concat(context.DataContainer.Routes .SelectMany(r => r.Markers) .Select(m => m.Latlng) ) .ToArray(); context.DataContainer.NorthEast = new LatLng(allLocations.Max(l => l.Lat), allLocations.Max(l => l.Lng)); context.DataContainer.SouthWest = new LatLng(allLocations.Min(l => l.Lat), allLocations.Min(l => l.Lng)); } var n = Math.Pow(2, MAX_ZOOM); var xNorthEast = GetXTile(context.DataContainer.NorthEast.Lng, n); var yNorthEast = GetYTile(context.DataContainer.NorthEast.Lat, n); var xSouthWest = GetXTile(context.DataContainer.SouthWest.Lng, n); var ySouthWest = GetYTile(context.DataContainer.SouthWest.Lat, n); var ratio = context.Width * 1.0 / context.Height; if (xNorthEast - xSouthWest > ratio * (ySouthWest - yNorthEast)) { var desiredY = (xNorthEast - xSouthWest) / ratio; var delatY = (desiredY - (ySouthWest - yNorthEast)) / 2; ySouthWest += delatY; yNorthEast -= delatY; } else { var desiredX = (ySouthWest - yNorthEast) * ratio; var delatX = (desiredX - (xNorthEast - xSouthWest)) / 2; xNorthEast += delatX; xSouthWest -= delatX; } context.DataContainer.NorthEast.Lng = GetLongitude(xNorthEast, n); context.DataContainer.NorthEast.Lat = GetLatitude(yNorthEast, n); context.DataContainer.SouthWest.Lng = GetLongitude(xSouthWest, n); context.DataContainer.SouthWest.Lat = GetLatitude(ySouthWest, n); }
///<inheritdoc /> public async Task <byte[]> Create(DataContainer dataContainer, int width, int height) { var context = new ImageCreationContext { Width = width, Height = height, DataContainer = dataContainer }; UpdateCorners(context); UpdateZoom(context); UpdateAddressesTemplates(context); await UpdateBackGroundImage(context); DrawRoutesOnImage(context); CropAndResizeImage(context); var imageStream = new MemoryStream(); context.Image.SaveAsPng(imageStream); return(imageStream.ToArray()); }
/// <summary> /// This will draw on the backgroup image the route and markers according to color and opacity /// </summary> /// <param name="context"></param> private void DrawRoutesOnImage(ImageCreationContext context) { context.Image.Mutate(ctx => { var routeColorIndex = 0; foreach (var route in context.DataContainer.Routes) { var points = route.Segments.SelectMany(s => s.Latlngs).Select(l => ConvertLatLngToPoint(l, context)).ToArray(); var markerPoints = route.Markers.Select(m => ConvertLatLngToPoint(m.Latlng, context)); var lineColor = _routeColors[routeColorIndex++]; routeColorIndex = routeColorIndex % _routeColors.Length; if (!string.IsNullOrEmpty(route.Color)) { lineColor = FromColorString(route.Color, route.Opacity); } if (points.Any()) { var path = new SixLabors.Shapes.Path(new LinearLineSegment(points)); ctx.Draw(Color.White, PEN_WIDTH + PEN_WIDTH_OFFSET, path); ctx.Draw(lineColor, PEN_WIDTH, path); var startCircle = new EllipsePolygon(points.First(), CIRCLE_RADIUS); ctx.Fill(Color.White, startCircle); ctx.Draw(Color.Green, CIRCLE_OUTLINE_WIDTH, startCircle); var endCircle = new EllipsePolygon(points.Last(), CIRCLE_RADIUS); ctx.Fill(Color.White, endCircle); ctx.Draw(Color.Red, CIRCLE_OUTLINE_WIDTH, endCircle); } foreach (var markerPoint in markerPoints) { var markerEllipse = new EllipsePolygon(markerPoint, CIRCLE_RADIUS); ctx.Fill(Color.White, markerEllipse); ctx.Draw(lineColor, PEN_WIDTH, markerEllipse); } } }); }
///<inheritdoc /> public async Task <byte[]> Create(DataContainer dataContainer, int width, int height) { _logger.LogDebug("Creating image for thumbnail started."); var context = new ImageCreationContext { Width = width, Height = height, DataContainer = dataContainer }; UpdateCorners(context); UpdateZoom(context); UpdateAddressesTemplates(context); await UpdateBackGroundImage(context); DrawRoutesOnImage(context); CropAndResizeImage(context); var imageStream = new MemoryStream(); context.Image.Save(imageStream, ImageFormat.Png); _logger.LogDebug("Creating image for thumbnail completed."); return(imageStream.ToArray()); }