public async Task GeostationaryUnderlay() { var definition = SatelliteRegistry.Locate(Goes16DefinitionPrefix); Assert.NotNull(definition, "Unable to find satellite definition"); var options = new UnderlayProjectionData( ProjectionType.Geostationary, InterpolationType.NearestNeighbour, "underlay.jpg", 5424, 1000); var underlay = await UnderlayService.GetUnderlayAsync(options, definition); underlay.Width.Should().Be(1000); underlay.Height.Should().Be(1000); // Retrieve cached underlay = await UnderlayService.GetUnderlayAsync(options, definition); underlay.Width.Should().Be(1000); underlay.Height.Should().Be(1000); // Verify changing options doesn't retrieve cached underlay options = new UnderlayProjectionData( ProjectionType.Geostationary, InterpolationType.NearestNeighbour, "underlay.jpg", 5424, 1500); underlay = await UnderlayService.GetUnderlayAsync(options, definition); underlay.Width.Should().Be(1500); underlay.Height.Should().Be(1500); }
/// <summary> /// Optionally resizes the underlay based on the target height. /// </summary> private void Resize(UnderlayProjectionData data, Image <Rgba32> underlay) { if (data.TargetSize == null) { return; } // Resize underlay to target image size underlay.Mutate(c => c.Resize(data.TargetSize.Value)); }
public async Task EquirectangularUnderlay() { var definition = SatelliteRegistry.Locate(Goes16DefinitionPrefix); Assert.NotNull(definition, "Unable to find satellite definition"); var options = new UnderlayProjectionData( ProjectionType.Equirectangular, InterpolationType.NearestNeighbour, "underlay.jpg", 5424); var underlay = await UnderlayService.GetUnderlayAsync(options, definition); underlay.Width.Should().Be(10848); underlay.Height.Should().Be(5424); }
/// <summary> /// Optionally resizes the underlay based on the target height. /// </summary> private void Resize(UnderlayProjectionData data, Image <Rgba32> underlay) { if (data.TargetHeight == null) { return; } // Resize underlay to target image size var targetHeight = data.TargetHeight.Value; // Ensure correct aspect ratio var targetWidth = (int)Math.Round(underlay.Width / (float)underlay.Height * targetHeight); _logger.LogInformation("Resizing underlay to {width} x {height} px", targetWidth, targetHeight); underlay.Mutate(c => c.Resize(targetWidth, targetHeight)); }
public async Task EquirectangularUnderlayWithCrop() { var(definition, _) = SatelliteRegistry.Locate(Goes16DefinitionPrefix); Assert.NotNull(definition, "Unable to find satellite definition"); var options = new UnderlayProjectionData( ProjectionType.Equirectangular, InterpolationType.NearestNeighbour, "underlay.jpg", 5424, latitudeCrop: new Range(Angle.FromDegrees(45), Angle.FromDegrees(-45))); RenderOptions.EquirectangularRender = new EquirectangularRenderOptions(false, false, false); var underlay = await UnderlayService.GetUnderlayAsync(options, definition); underlay.Width.Should().Be(10848); underlay.Height.Should().Be(2712); }
/// <summary> /// Adds an underlay registration to the database. /// </summary> /// <param name="definition">optional satellite definition used to generate underlay</param> /// <param name="data">underlay rendering options</param> /// <param name="underlayPath">path to rendered underlay</param> public async Task RegisterCacheAsync(SatelliteDefinition?definition, UnderlayProjectionData data, string underlayPath) { await using var connection = new SqliteConnection(ConnectionString); await connection.OpenAsync(); string sql = definition == null ? "INSERT INTO UnderlayCache(Filename, Configuration, Timestamp) VALUES(@Filename, @Configuration, @Timestamp)" : "INSERT INTO UnderlayCache(Filename, Configuration, Longitude, Timestamp) VALUES(@Filename, @Configuration, @Longitude, @Timestamp)"; var timestamp = File.GetLastWriteTimeUtc(underlayPath); await connection.ExecuteAsync(sql, new { Filename = underlayPath, Configuration = JsonConvert.SerializeObject(data), Longitude = definition?.Longitude, Timestamp = (DateTimeOffset)timestamp }); }
public async Task EquirectangularUnderlayNoCrop() { var(definition, _) = SatelliteRegistry.Locate(Goes16DefinitionPrefix); Assert.NotNull(definition, "Unable to find satellite definition"); var data = new UnderlayProjectionData( ProjectionType.Equirectangular, InterpolationType.NearestNeighbour, "underlay.jpg", 1000, new Size(5424, 5424), new Range(0, Math.PI / 2)); RenderOptions.EquirectangularRender = new EquirectangularRenderOptions(false, true, false); var underlay = await UnderlayService.GetUnderlayAsync(data, definition); underlay.Width.Should().Be(5424); underlay.Height.Should().Be(5424); }
/// <summary> /// Retrieves a full-colour underlay image with the target projection. Underlays are cached to disk to speed up /// computation. /// </summary> /// <param name="data">Underlay generation options</param> /// <param name="definition">Optional satellite definition, if projecting underlay to match a satellite IR image</param> /// <returns>projected underlay</returns> public async Task <Image <Rgba32> > GetUnderlayAsync(UnderlayProjectionData data, SatelliteDefinition?definition = null) { // Attempt to retrieve underlay from cache var cached = await _cache.GetUnderlayAsync(definition, data); if (cached != null) { return(cached); } // Load master equirectangular underlay image from disk var underlay = await Image.LoadAsync <Rgba32>(_options.UnderlayPath); // Project to match satellite imagery var target = GetProjected(underlay, definition, data); Resize(data, target); // Register underlay in cache await _cache.SetUnderlayAsync(target, definition, data); return(target); }
public override async Task <ExecutionResult> RunAsync(IStepExecutionContext context) { Guard.Against.Null(_options.GeostationaryRender, nameof(_options.GeostationaryRender)); Guard.Against.Null(Registration?.Image, nameof(Registration.Image)); if (_options.NoUnderlay) { TargetImage = Registration.Image.Clone(); return(ExecutionResult.Next()); } // Get or generate projected underlay var underlayOptions = new UnderlayProjectionData( ProjectionType.Geostationary, _options.InterpolationType, _options.UnderlayPath, _options.ImageSize); _logger.LogInformation("Retrieving underlay"); var underlay = await _underlayService.GetUnderlayAsync(underlayOptions, Registration.Definition); _logger.LogInformation("Tinting and normalising IR imagery"); if (_options.AutoAdjustLevels) { Registration.Image.AdjustLevels(); } TargetImage = Registration.Image.Clone(); TargetImage.Tint(_options.Tint); _logger.LogInformation("Blending with underlay"); TargetImage.Mutate(c => c .Resize(_options.ImageSize, _options.ImageSize) .DrawImage(underlay, PixelColorBlendingMode.Screen, 1.0f)); return(ExecutionResult.Next()); }
/// <summary> /// Retrieves an underlay image from the cache. /// </summary> public async Task <Image <Rgba32>?> GetUnderlayAsync(SatelliteDefinition?definition, UnderlayProjectionData data) { var metadata = await _repository.GetCacheMetadataAsync(definition, data); if (metadata == null) { return(null); } var path = metadata.Filename; var fullPath = Path.Combine(_cachePath, path); // Remove underlay registration from cache if it has been deleted from disk if (!File.Exists(fullPath)) { _logger.LogWarning("Cache file {path} not found; removing from cache registration", path); await _repository.ClearCacheEntryAsync(path); } var fileTimestamp = File.GetLastWriteTimeUtc(_options.UnderlayPath); // Check if the source underlay file has been updated since the cache was created if (metadata.Timestamp < fileTimestamp) { _logger.LogInformation("Timestamp of underlay changed; updating cache"); await _repository.ClearCacheEntryAsync(path); return(null); } try { if (definition == null) { _logger.LogInformation("Using cached underlay"); } else { _logger.LogInformation("{definition:l0} Using cached underlay", definition.DisplayName); } return(await Image.LoadAsync <Rgba32>(fullPath)); } catch (Exception e) { // Remove underlay registration from cache if there was an error reading it from disk _logger.LogWarning(e, "Cache file {path} unable to be read; removing from cache registration", path); await _repository.ClearCacheEntryAsync(path); } return(null); }
/// <summary> /// Sets an underlay image to the cache. /// </summary> public async Task SetUnderlayAsync(Image <Rgba32> underlay, SatelliteDefinition?definition, UnderlayProjectionData data) { if (definition == null) { _logger.LogInformation("Caching underlay"); } else { _logger.LogInformation("{definition:l0} Caching underlay", definition.DisplayName); } // Save underlay to disk var filename = $"{Guid.NewGuid()}.jpg"; var underlayPath = Path.Combine(_cachePath, filename); await underlay.SaveAsync(underlayPath); // Register underlay path in the cache await _repository.RegisterCacheAsync(definition, data, underlayPath); }
/// <summary> /// Returns an underlay projected and optionally cropped. /// </summary> private Image <Rgba32> GetProjected(Image <Rgba32> underlay, SatelliteDefinition?definition, UnderlayProjectionData data) { switch (data.Projection) { case ProjectionType.Geostationary: if (definition == null) { throw new InvalidOperationException("Satellite definition must be provided for geostationary projection"); } // Project underlay to geostationary, based on the target satellite _logger.LogInformation("{definition:l0} Rendering geostationary underlay", definition.DisplayName); return(underlay.ToGeostationaryProjection(definition.Longitude, definition.Height, _options)); case ProjectionType.Equirectangular: // Optionally crop and offset to specified lat/long range if (data.CropSpecified) { Offset(underlay, data.LongitudeCrop !.Value); Crop(underlay, data.LatitudeCrop !.Value); } return(underlay); default: throw new ArgumentOutOfRangeException($"Unhandled projection type: {data.Projection}"); } }
/// <summary> /// Returns an underlay projected and optionally cropped. /// </summary> private Image <Rgba32> GetProjected(Image <Rgba32> underlay, SatelliteDefinition?definition, UnderlayProjectionData data) { switch (data.Projection) { case ProjectionType.Geostationary: if (definition == null) { throw new InvalidOperationException("Satellite definition must be provided for geostationary projection"); } // Project underlay to geostationary, based on the target satellite _logger.LogInformation("{Definition:l0} Rendering geostationary underlay", definition.DisplayName); return(underlay.ToGeostationaryProjection(definition.Longitude, definition.Height, _options)); case ProjectionType.Equirectangular: // Perform latitude crop to match IR imagery if required var equirectangularOptions = _options.EquirectangularRender; if (equirectangularOptions == null || !equirectangularOptions.NoCrop && !equirectangularOptions.ExplicitCrop) { Crop(underlay, data.LatitudeCrop !.Value); } return(underlay); default: throw new ArgumentOutOfRangeException($"Unhandled projection type: {data.Projection}"); } }
/// <summary> /// Retrieves a cached underlay filename and timestamp based on an optional satellite definition and render options, or /// <c>null</c> if the underlay isn't in the cache. /// </summary> public async Task <UnderlayMetadata?> GetCacheMetadataAsync(SatelliteDefinition?definition, UnderlayProjectionData data) { await using var connection = new SqliteConnection(ConnectionString); await connection.OpenAsync(); var sql = "SELECT Filename, Timestamp FROM UnderlayCache WHERE Configuration=@Configuration"; if (definition != null) { sql += " AND Longitude=@Longitude"; } return(await connection.QueryFirstOrDefaultAsync <UnderlayMetadata>(sql, new { definition?.Longitude, Configuration = JsonConvert.SerializeObject(data) })); }