use std::io::{Cursor, Read, Write};
use std::path::Path;
use zip::write::FileOptions;
use zip::{CompressionMethod, ZipArchive, ZipWriter};
use crate::error::{GeoError, Result};
use crate::feature::Layer;
use crate::reproject;
pub fn read<P: AsRef<Path>>(path: P) -> Result<Layer> {
let bytes = std::fs::read(path).map_err(GeoError::Io)?;
from_bytes(&bytes)
}
pub fn from_bytes(bytes: &[u8]) -> Result<Layer> {
let cursor = Cursor::new(bytes);
let mut archive = ZipArchive::new(cursor)
.map_err(|e| GeoError::Kmz(format!("invalid zip archive: {e}")))?;
let mut preferred_index: Option<usize> = None;
let mut fallback_index: Option<usize> = None;
for i in 0..archive.len() {
let f = archive
.by_index(i)
.map_err(|e| GeoError::Kmz(format!("failed reading zip entry {i}: {e}")))?;
let name = f.name().to_ascii_lowercase();
if name == "doc.kml" {
preferred_index = Some(i);
break;
}
if fallback_index.is_none() && name.ends_with(".kml") {
fallback_index = Some(i);
}
}
let index = preferred_index
.or(fallback_index)
.ok_or_else(|| GeoError::Kmz("no .kml entry found in KMZ".into()))?;
let mut kml_text = String::new();
let mut f = archive
.by_index(index)
.map_err(|e| GeoError::Kmz(format!("failed opening KML entry {index}: {e}")))?;
f.read_to_string(&mut kml_text)
.map_err(|e| GeoError::Kmz(format!("failed reading KML entry {index}: {e}")))?;
crate::kml::parse_str(&kml_text)
}
pub fn write<P: AsRef<Path>>(layer: &Layer, path: P) -> Result<()> {
let bytes = to_bytes(layer)?;
std::fs::write(path, bytes).map_err(GeoError::Io)
}
pub fn to_bytes(layer: &Layer) -> Result<Vec<u8>> {
let out_layer = prepare_kmz_layer(layer)?;
let kml = crate::kml::to_string(&out_layer);
let mut cursor = Cursor::new(Vec::<u8>::new());
{
let mut writer = ZipWriter::new(&mut cursor);
let opts = FileOptions::default().compression_method(CompressionMethod::Deflated);
writer
.start_file("doc.kml", opts)
.map_err(|e| GeoError::Kmz(format!("failed creating doc.kml entry: {e}")))?;
writer
.write_all(kml.as_bytes())
.map_err(|e| GeoError::Kmz(format!("failed writing doc.kml: {e}")))?;
writer
.finish()
.map_err(|e| GeoError::Kmz(format!("failed finalizing KMZ archive: {e}")))?;
}
Ok(cursor.into_inner())
}
fn prepare_kmz_layer(layer: &Layer) -> Result<Layer> {
if layer.crs_epsg() == Some(4326) {
return Ok(layer.clone());
}
if layer.crs_epsg().is_some() || layer.crs_wkt().is_some() {
return reproject::layer_to_epsg(layer, 4326);
}
Ok(layer.clone())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::feature::{FieldDef, FieldType};
use crate::geometry::{Geometry, GeometryType};
#[test]
fn kmz_roundtrip_bytes() {
let mut layer = Layer::new("places")
.with_geom_type(GeometryType::Point)
.with_epsg(4326);
layer.add_field(FieldDef::new("name", FieldType::Text));
layer
.add_feature(Some(Geometry::point(-0.1278, 51.5074)), &[("name", "London".into())])
.unwrap();
let bytes = to_bytes(&layer).unwrap();
let parsed = from_bytes(&bytes).unwrap();
assert_eq!(parsed.len(), 1);
assert!(matches!(parsed.features[0].geometry, Some(Geometry::Point(_))));
assert_eq!(parsed.features[0].get(&parsed.schema, "name").unwrap().as_str(), Some("London"));
}
#[test]
fn kmz_write_reprojects_projected_layer_to_epsg4326() {
let mut layer = Layer::new("mercator").with_crs_epsg(3857);
layer
.add_feature(Some(Geometry::point(111_319.49079327357, 0.0)), &[])
.unwrap();
let bytes = to_bytes(&layer).unwrap();
let parsed = from_bytes(&bytes).unwrap();
assert_eq!(parsed.crs_epsg(), Some(4326));
if let Some(Geometry::Point(c)) = &parsed.features[0].geometry {
assert!((c.x - 1.0).abs() < 1.0e-5);
assert!(c.y.abs() < 1.0e-9);
} else {
panic!("expected Point geometry");
}
}
}