/// <summary>
        /// Validates the request to create a new trail
        /// </summary>
        public ValidationServiceResponse <TopoTrailInfo> ValidateCreate(ITopoTrailUpdateRequest request)
        {
            // load existing trail
            var response = new ValidationServiceResponse <TopoTrailInfo>(null);

            // do basic validation
            if (String.IsNullOrWhiteSpace(request.Name))
            {
                response.AddError("Name", "Name is required!");
            }

            // TODO: REFACTOR: use GeoPolitical
            var timezone = GeoTimezoneInfo.Find(request.Timezone);

            if (timezone == null)
            {
                response.AddError("Timezone", "Timezone is missing or invalid!");
            }

            var country = GeoCountryInfo.Find(request.Country);

            if (country == null)
            {
                response.AddError("Country", "Country is missing or invalid!");
            }

            var region = GeoRegionInfo.Find(request.Region);

            if (region == null && !String.IsNullOrWhiteSpace(request.Region))
            {
                response.AddError("Region", "Region is invalid!");
            }

            return(response);
        }
        /// <summary>
        /// Validates the data that will be used to create or update a place
        /// </summary>
        public ValidationServiceResponse <CartoPlaceInfo> ValidatePlace(CartoPlaceData data)
        {
            var place    = CreateCleanPlaceInfo(data);
            var response = new ValidationServiceResponse <CartoPlaceInfo>(place);

            // validate place info
            if (String.IsNullOrWhiteSpace(place.Name))
            {
                response.AddError("Name", "Place name is missing!");
            }

            // validate political info
            var political = new GeoPolitical(data);

            // country is required
            if (!political.IsCountryValid())
            {
                response.AddError("Country", "Country is missing or invalid!");
            }

            // region is only required if specified
            if (!political.IsRegionOptionalValid())
            {
                response.AddError("Region", "Region is invalid!");
            }

            // timezone cannot be empty or UTC
            if (!political.IsTimezoneOptionalButNotUTCValid())
            {
                response.AddError("Timezone", "Timezone is invalid!");
            }

            // TODO: check center/bounds


            // basic validation
            return(response);
        }
        /// <summary>
        /// Create the trail metadata, file and cache
        /// </summary>
        public string CreateTrail(TopoTrailInfo trail)
        {
            var response = new ValidationServiceResponse <TopoTrailInfo>(trail);

            // check file system
            if (!Directory.Exists(_rootUri))
            {
                throw new Exception($"Directory not initalized: {_rootUri}");
            }
            var folder = Path.Combine(_rootUri, trail.Country.Name);

            if (!Directory.Exists(folder))
            {
                Directory.CreateDirectory(folder);
            }

            // generate new and rename/remove existing files
            var filename = GetFilename(trail);

            // check if overwrite file
            if (File.Exists(filename))
            {
                throw new Exception($"{filename} already exists!");
            }

            // write new file
            var contents = BuildGpx(trail);

            File.WriteAllText(filename, contents);

            // add new file to cache
            trail.Key = Path.GetFileNameWithoutExtension(filename).ToUpperInvariant();
            _trails.Add(trail);

            // return new key
            return(trail.Key);
        }