private Location3DModelResponse ParseResultCsvLine(string cols, char separator)
        {
            string[] values = cols.Split(separator);
            //pk,	pn,	lat,	lon,	link,	tilecount_running_total,	sketchfab_status	, sketchfab_id

            int index = 0;
            Location3DModelRequest request = new Location3DModelRequest();

            request.Id          = values[index++];
            request.Title       = values[index++].Trim('\"');
            request.Latitude    = double.Parse(values[index++], CultureInfo.InvariantCulture);
            request.Longitude   = double.Parse(values[index++], CultureInfo.InvariantCulture);
            request.Description = values[index++];
            Location3DModelResponse result = new Location3DModelResponse();

            result.Request = request;
            result.Id      = request.Id;


            result.NumTiles = int.Parse(values[index++]);
            if (Enum.TryParse <UploadStatus>(values[index++], true, out UploadStatus uploadStatus))
            {
                result.UploadStatus = uploadStatus;
            }
            result.UploadedFileId = values[index++];

            return(result);
        }
        private Dictionary <string, Location3DModelResponse> ParseOutputFile(string fileName)
        {
            Dictionary <string, Location3DModelResponse> results = new Dictionary <string, Location3DModelResponse>();

            if (File.Exists(fileName))
            {
                using (StreamReader sr = new StreamReader(fileName, Encoding.UTF8))
                {
                    sr.ReadLine(); // skip header
                    do
                    {
                        //pk,pn,lat,lon,link
                        Location3DModelResponse result = ParseResultCsvLine(sr.ReadLine(), '\t');
                        results.Add(result.Id, result);
                    } while (!sr.EndOfStream);
                }
            }

            return(results);
        }
        public void BatchGenerationAndUpload(string fileName, string outputDirName)
        {
            Location3DModelSettings settings = new Location3DModelSettings()
            {
                Dataset                = DEMDataSet.NASADEM,
                ImageryProvider        = ImageryProvider.ThunderForestLandscape,
                ZScale                 = 2f,
                SideSizeKm             = 1.5f,
                OsmBuildings           = true,
                DownloadMissingFiles   = false,
                GenerateTIN            = false,
                MaxDegreeOfParallelism = 1,
                OutputDirectory        = Path.Combine(Directory.GetCurrentDirectory(), outputDirName)
            };

            string currentOutFilePath = string.Concat(Path.ChangeExtension(fileName, null), $"_out.txt");
            Dictionary <string, Location3DModelRequest>  requests  = ParseInputFile(fileName);
            Dictionary <string, Location3DModelResponse> responses = ParseOutputFile(currentOutFilePath);

            // Backup file by creating a copy
            var outFilePath = string.Concat(Path.ChangeExtension(fileName, null), $"_out_{DateTime.Now:ddMMyyyy-hhmmss}.txt");

            bool append = responses.Count > 0;

            _logger.LogInformation($"Append mode: {append}");

            // Restart from previous run
            Directory.CreateDirectory(settings.OutputDirectory);
            // Filter already generated files
            int countBefore = requests.Count;

            //requests = requests.Where(r => !File.Exists(Path.Combine(settings.OutputDirectory, settings.ModelFileNameGenerator(settings, r.Value)))).ToList();
            if (requests.Count < countBefore)
            {
                _logger.LogInformation($"Skipping {countBefore - requests.Count} files already generated.");
            }


            // Generate and upload
            int sumTilesDownloaded = 0;

            using (StreamWriter sw = new StreamWriter(outFilePath, append: append, Encoding.UTF8))
            {
                sw.WriteLine(string.Join("\t", "pk", "pn", "lat", "lon", "link", "tilecount_running_total", "sketchfab_status", "sketchfab_id"));

                foreach (var request in requests.Values)
                {
                    UploadModelRequest uploadRequest;
                    try
                    {
                        bool modelExists = File.Exists(Path.Combine(settings.OutputDirectory, settings.ModelFileNameGenerator(settings, request)));
                        Location3DModelResponse response = null;
                        if (!(modelExists && responses.TryGetValue(request.Id, out response))) // check model file exist
                        {
                            try
                            {
                                //===========================
                                // Generation
                                response               = Generate3DLocationModel(request, settings);
                                sumTilesDownloaded    += response.NumTiles ?? 0;
                                response.Id            = request.Id;
                                responses[response.Id] = response;
                            }
                            catch (Exception ex)
                            {
                                _logger.LogError(ex.Message);
                            }
                            finally
                            {
                                _logger.LogInformation("Model generated. Waiting 10s...");
                                Thread.Sleep(10000); // wait 2 sec to dive overpassAPI some breath
                            }
                        }

                        if (response != null && string.IsNullOrWhiteSpace(response.UploadedFileId))
                        {
                            try
                            {
                                //===========================
                                // Upload
                                uploadRequest = GetUploadRequest(settings, request);
                                var sfResponse = _sketchFabApi.UploadModelAsync(uploadRequest, _sketchFabToken).GetAwaiter().GetResult();
                                response.UploadedFileId = sfResponse.ModelId;
                                response.UploadStatus   = sfResponse.StatusCode == HttpStatusCode.Created ? UploadStatus.OK : UploadStatus.Error;
                                _logger.LogInformation($"Sketchfab upload ok : {response.UploadedFileId}");
                            }
                            catch (Exception ex)
                            {
                                response.UploadStatus   = UploadStatus.Error;
                                response.UploadedFileId = null;
                                _logger.LogError(ex.Message);
                            }
                            finally
                            {
                                _logger.LogInformation($"Waiting 10s...");
                                Thread.Sleep(10000); // wait 2 sec to give SkecthFab some breath
                            }
                        }

                        sw.WriteLine(string.Join("\t", request.Id, request.Title, request.Latitude, request.Longitude
                                                 , request.Description              // link
                                                 , sumTilesDownloaded               // tilecount_running_total
                                                 , response.UploadStatus.ToString() // sketchfab_status
                                                 , response.UploadedFileId          // sketchfab_id
                                                 ));

                        sw.Flush();
                        if (responses.Count > 0)
                        {
                            _logger.LogInformation($"Reponse: {responses.Last().Value.Elapsed.TotalSeconds:N3} s, Average: {responses.Average(r => r.Value.Elapsed.TotalSeconds):N3} s ({responses.Count}/{requests.Count} model(s) so far, {sumTilesDownloaded} tiles)");
                        }
                    }
                    catch (Exception ex)
                    {
                        _logger.LogError(ex.Message);
                    }
                }
            }
        }
        private Location3DModelResponse Generate3DLocationModel(Location3DModelRequest request, Location3DModelSettings settings)
        {
            Location3DModelResponse response = new Location3DModelResponse();

            try
            {
                bool imageryFailed = false;
                using (TimeSpanBlock timer = new TimeSpanBlock($"3D model {request.Id}", _logger))
                {
                    BoundingBox bbox = GetBoundingBoxAroundLocation(request.Latitude, request.Longitude, settings.SideSizeKm);

                    HeightMap hMap      = _elevationService.GetHeightMap(ref bbox, settings.Dataset);
                    var       transform = new ModelGenerationTransform(bbox, Reprojection.SRID_PROJECTED_MERCATOR, centerOnOrigin: true, settings.ZScale, centerOnZOrigin: true);

                    response.Attributions.AddRange(settings.Attributions);   // will be added to the model
                    response.Attributions.Add(settings.Dataset.Attribution); // will be added to the model


                    PBRTexture pbrTexture = null;
                    if (settings.ImageryProvider != null)
                    {
                        response.Attributions.Add(settings.ImageryProvider.Attribution); // will be added to the model

                        // Imagery
                        TileRange tiles = _imageryService.ComputeBoundingBoxTileRange(bbox, settings.ImageryProvider, settings.MinTilesPerImage);
                        Debug.Assert(tiles.Count < 400);

                        tiles = _imageryService.DownloadTiles(tiles, settings.ImageryProvider);

                        string      fileName = Path.Combine(settings.OutputDirectory, $"{request.Id}_Texture.jpg");
                        TextureInfo texInfo  = _imageryService.ConstructTexture(tiles, bbox, fileName, TextureImageFormat.image_jpeg);

                        transform.BoundingBox = bbox;
                        hMap = transform.TransformHeightMap(hMap);


                        //var normalMap = _imageryService.GenerateNormalMap(hMap, settings.OutputDirectory, $"{request.Id}_normalmap.png");
                        pbrTexture = PBRTexture.Create(texInfo);
                    }

                    // Center on origin
                    //hMap = hMap.CenterOnOrigin(out Matrix4x4 transform).BakeCoordinates();
                    //response.Origin = new GeoPoint(request.Latitude, request.Longitude).ReprojectTo(Reprojection.SRID_GEODETIC, Reprojection.SRID_PROJECTED_MERCATOR);

                    ModelRoot model = _gltfService.CreateNewModel();

                    //=======================
                    // Buildings
                    if (settings.OsmBuildings)
                    {
                        model = _sampleOsmProcessor.Run(model, OsmLayer.Buildings, bbox, transform, computeElevations: true, settings.Dataset, settings.DownloadMissingFiles);
                    }


                    if (settings.GenerateTIN)
                    {
                        model = AddTINMesh(model, hMap, 2d, _gltfService, pbrTexture, Reprojection.SRID_PROJECTED_MERCATOR);
                    }
                    else
                    {
                        model = _gltfService.AddTerrainMesh(model, hMap, pbrTexture);
                    }
                    model.Asset.Generator = "DEM Net Elevation API with SharpGLTF";
                    //model.TryUseExtrasAsList(true).AddRange(response.Attributions);
                    model.SaveGLB(Path.Combine(settings.OutputDirectory, string.Concat(imageryFailed ? "imageryFailed_" : "", settings.ModelFileNameGenerator(settings, request))));

                    // cleanup
                    //if (pbrTexture != null)
                    //{
                    //    if (pbrTexture.NormalTexture != null) File.Delete(pbrTexture.NormalTexture.FilePath);
                    //    File.Delete(pbrTexture.BaseColorTexture.FilePath);
                    //}

                    response.Elapsed  = timer.Elapsed;
                    response.NumTiles = pbrTexture.BaseColorTexture.TileCount;
                }
            }
            catch (Exception)
            {
                throw;
            }
            return(response);
        }