1use crate::error::{ServiceError, ServiceResult};
7use crate::wfs::database::{
8 BboxFilter, CountCacheConfig, CqlFilter, DatabaseFeatureCounter, DatabaseSource, DatabaseType,
9};
10use crate::wfs::{FeatureSource, WfsState};
11use axum::{
12 http::header,
13 response::{IntoResponse, Response},
14};
15use serde::Deserialize;
16
17#[derive(Debug, Deserialize)]
19#[serde(rename_all = "UPPERCASE")]
20pub struct GetFeatureParams {
21 #[serde(rename = "TYPENAME", alias = "TYPENAMES")]
23 pub type_names: String,
24 #[serde(default = "default_output_format")]
26 pub output_format: String,
27 #[serde(rename = "COUNT", alias = "MAXFEATURES")]
29 pub count: Option<usize>,
30 #[serde(default = "default_result_type")]
32 pub result_type: String,
33 pub bbox: Option<String>,
35 pub filter: Option<String>,
37 pub property_name: Option<String>,
39 pub sortby: Option<String>,
41 #[serde(rename = "STARTINDEX")]
43 pub start_index: Option<usize>,
44 pub srsname: Option<String>,
46}
47
48fn default_output_format() -> String {
49 "application/gml+xml; version=3.2".to_string()
50}
51
52fn default_result_type() -> String {
53 "results".to_string()
54}
55
56#[derive(Debug, Deserialize)]
58#[serde(rename_all = "UPPERCASE")]
59pub struct DescribeFeatureTypeParams {
60 #[serde(rename = "TYPENAME", alias = "TYPENAMES")]
62 pub type_names: Option<String>,
63 #[serde(default = "default_schema_format")]
65 pub output_format: String,
66}
67
68fn default_schema_format() -> String {
69 "application/gml+xml; version=3.2".to_string()
70}
71
72pub async fn handle_get_feature(
74 state: &WfsState,
75 _version: &str,
76 params: &serde_json::Value,
77) -> Result<Response, ServiceError> {
78 let params: GetFeatureParams = serde_json::from_value(params.clone())
79 .map_err(|e| ServiceError::InvalidParameter("Parameters".to_string(), e.to_string()))?;
80
81 let type_names: Vec<&str> = params.type_names.split(',').collect();
83
84 for type_name in &type_names {
86 if state.get_feature_type(type_name.trim()).is_none() {
87 return Err(ServiceError::NotFound(format!(
88 "Feature type not found: {}",
89 type_name
90 )));
91 }
92 }
93
94 if params.result_type.to_lowercase() == "hits" {
96 return generate_hits_response(state, &type_names, ¶ms);
97 }
98
99 match params.output_format.as_str() {
101 "application/json" | "application/geo+json" => {
102 generate_geojson_response(state, &type_names, ¶ms).await
103 }
104 _ => generate_gml_response(state, &type_names, ¶ms).await,
105 }
106}
107
108pub async fn handle_describe_feature_type(
110 state: &WfsState,
111 _version: &str,
112 params: &serde_json::Value,
113) -> Result<Response, ServiceError> {
114 let params: DescribeFeatureTypeParams = serde_json::from_value(params.clone())
115 .map_err(|e| ServiceError::InvalidParameter("Parameters".to_string(), e.to_string()))?;
116
117 let type_names: Vec<String> = if let Some(ref names) = params.type_names {
118 names.split(',').map(|s| s.trim().to_string()).collect()
119 } else {
120 state
122 .feature_types
123 .iter()
124 .map(|entry| entry.key().clone())
125 .collect()
126 };
127 let type_names_refs: Vec<&str> = type_names.iter().map(|s| s.as_str()).collect();
128
129 generate_feature_schema(state, &type_names_refs)
130}
131
132static FEATURE_COUNTER: std::sync::OnceLock<DatabaseFeatureCounter> = std::sync::OnceLock::new();
134
135fn get_feature_counter() -> &'static DatabaseFeatureCounter {
137 FEATURE_COUNTER.get_or_init(|| DatabaseFeatureCounter::new(CountCacheConfig::default()))
138}
139
140fn generate_hits_response(
142 state: &WfsState,
143 type_names: &[&str],
144 params: &GetFeatureParams,
145) -> Result<Response, ServiceError> {
146 let rt = tokio::runtime::Handle::try_current().map_err(|_| {
148 ServiceError::Internal("No async runtime available for database operations".to_string())
149 })?;
150
151 let mut total_count = 0usize;
152 let mut any_estimated = false;
153
154 for type_name in type_names {
155 let ft = state
156 .get_feature_type(type_name.trim())
157 .ok_or_else(|| ServiceError::NotFound(format!("Feature type: {}", type_name)))?;
158
159 let (count, is_estimated) = match &ft.source {
160 FeatureSource::Memory(features) => {
161 let filtered = apply_memory_filters(features, params)?;
163 (filtered.len(), false)
164 }
165 FeatureSource::File(path) => {
166 let count = count_features_in_file_filtered(path, params)?;
168 (count, false)
169 }
170 FeatureSource::Database(conn_string) => {
171 let db_source = create_legacy_database_source(conn_string, type_name);
173 rt.block_on(count_database_features(&db_source, params))?
174 }
175 FeatureSource::DatabaseSource(db_source) => {
176 rt.block_on(count_database_features(db_source, params))?
178 }
179 };
180
181 total_count += count;
182 if is_estimated {
183 any_estimated = true;
184 }
185 }
186
187 if let Some(max_count) = params.count {
189 total_count = total_count.min(max_count);
190 }
191
192 let number_matched = if any_estimated {
194 format!("~{}", total_count)
195 } else {
196 total_count.to_string()
197 };
198
199 let xml = format!(
200 r#"<?xml version="1.0" encoding="UTF-8"?>
201<wfs:FeatureCollection
202 xmlns:wfs="http://www.opengis.net/wfs/2.0"
203 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
204 numberMatched="{}"
205 numberReturned="0"
206 timeStamp="{}">
207</wfs:FeatureCollection>"#,
208 number_matched,
209 chrono::Utc::now().to_rfc3339()
210 );
211
212 Ok(([(header::CONTENT_TYPE, "application/xml")], xml).into_response())
213}
214
215fn create_legacy_database_source(conn_string: &str, table_name: &str) -> DatabaseSource {
217 let db_type =
219 if conn_string.starts_with("postgresql://") || conn_string.starts_with("postgres://") {
220 DatabaseType::PostGis
221 } else if conn_string.starts_with("mysql://") {
222 DatabaseType::MySql
223 } else if conn_string.ends_with(".db") || conn_string.ends_with(".sqlite") {
224 DatabaseType::Sqlite
225 } else {
226 DatabaseType::Generic
227 };
228
229 DatabaseSource::new(conn_string, table_name).with_database_type(db_type)
230}
231
232async fn count_database_features(
234 source: &DatabaseSource,
235 params: &GetFeatureParams,
236) -> ServiceResult<(usize, bool)> {
237 let counter = get_feature_counter();
238
239 let bbox_filter = if let Some(ref bbox_str) = params.bbox {
241 Some(BboxFilter::from_bbox_string(bbox_str)?)
242 } else {
243 None
244 };
245
246 let cql_filter = params.filter.as_ref().map(CqlFilter::new);
248
249 let result = counter
251 .get_count(source, cql_filter.as_ref(), bbox_filter.as_ref())
252 .await?;
253
254 Ok((result.count, result.is_estimated))
255}
256
257fn apply_memory_filters<'a>(
259 features: &'a [geojson::Feature],
260 params: &'a GetFeatureParams,
261) -> ServiceResult<Vec<&'a geojson::Feature>> {
262 let mut filtered: Vec<&geojson::Feature> = features.iter().collect();
263
264 if let Some(ref bbox_str) = params.bbox {
266 let bbox = BboxFilter::from_bbox_string(bbox_str)?;
267 filtered.retain(|f| feature_in_bbox(f, &bbox));
268 }
269
270 if let Some(ref filter_str) = params.filter {
272 filtered.retain(|f| feature_matches_filter(f, filter_str));
273 }
274
275 Ok(filtered)
276}
277
278fn feature_in_bbox(feature: &geojson::Feature, bbox: &BboxFilter) -> bool {
280 if let Some(ref geom_bbox) = feature.bbox {
281 if geom_bbox.len() >= 4 {
282 return geom_bbox[0] <= bbox.max_x
283 && geom_bbox[2] >= bbox.min_x
284 && geom_bbox[1] <= bbox.max_y
285 && geom_bbox[3] >= bbox.min_y;
286 }
287 }
288
289 if let Some(ref geometry) = feature.geometry {
291 return geometry_intersects_bbox(geometry, bbox);
292 }
293
294 false
295}
296
297fn geometry_intersects_bbox(geometry: &geojson::Geometry, bbox: &BboxFilter) -> bool {
299 use geojson::GeometryValue;
300
301 match &geometry.value {
302 GeometryValue::Point {
303 coordinates: coords,
304 } => {
305 coords.len() >= 2
306 && coords[0] >= bbox.min_x
307 && coords[0] <= bbox.max_x
308 && coords[1] >= bbox.min_y
309 && coords[1] <= bbox.max_y
310 }
311 GeometryValue::MultiPoint {
312 coordinates: points,
313 } => points.iter().any(|coords| {
314 coords.len() >= 2
315 && coords[0] >= bbox.min_x
316 && coords[0] <= bbox.max_x
317 && coords[1] >= bbox.min_y
318 && coords[1] <= bbox.max_y
319 }),
320 GeometryValue::LineString {
321 coordinates: coords,
322 } => coords.iter().any(|c| {
323 c.len() >= 2
324 && c[0] >= bbox.min_x
325 && c[0] <= bbox.max_x
326 && c[1] >= bbox.min_y
327 && c[1] <= bbox.max_y
328 }),
329 GeometryValue::MultiLineString { coordinates: lines } => lines.iter().any(|line| {
330 line.iter().any(|c| {
331 c.len() >= 2
332 && c[0] >= bbox.min_x
333 && c[0] <= bbox.max_x
334 && c[1] >= bbox.min_y
335 && c[1] <= bbox.max_y
336 })
337 }),
338 GeometryValue::Polygon { coordinates: rings } => rings.iter().any(|ring| {
339 ring.iter().any(|c| {
340 c.len() >= 2
341 && c[0] >= bbox.min_x
342 && c[0] <= bbox.max_x
343 && c[1] >= bbox.min_y
344 && c[1] <= bbox.max_y
345 })
346 }),
347 GeometryValue::MultiPolygon {
348 coordinates: polygons,
349 } => polygons.iter().any(|polygon| {
350 polygon.iter().any(|ring| {
351 ring.iter().any(|c| {
352 c.len() >= 2
353 && c[0] >= bbox.min_x
354 && c[0] <= bbox.max_x
355 && c[1] >= bbox.min_y
356 && c[1] <= bbox.max_y
357 })
358 })
359 }),
360 GeometryValue::GeometryCollection { geometries: geoms } => {
361 geoms.iter().any(|g| geometry_intersects_bbox(g, bbox))
362 }
363 }
364}
365
366fn feature_matches_filter(feature: &geojson::Feature, filter_str: &str) -> bool {
368 let props = match &feature.properties {
372 Some(p) => p,
373 None => return false,
374 };
375
376 let filter = filter_str.trim();
378
379 if filter.contains('=')
381 && !filter.contains("!=")
382 && !filter.contains("<=")
383 && !filter.contains(">=")
384 {
385 let parts: Vec<&str> = filter.splitn(2, '=').collect();
386 if parts.len() == 2 {
387 let prop_name = parts[0].trim().trim_matches('"').trim_matches('\'');
388 let prop_value = parts[1].trim().trim_matches('\'').trim_matches('"');
389
390 if let Some(value) = props.get(prop_name) {
391 return match value {
392 serde_json::Value::String(s) => s == prop_value,
393 serde_json::Value::Number(n) => n.to_string() == prop_value,
394 serde_json::Value::Bool(b) => b.to_string() == prop_value,
395 _ => false,
396 };
397 }
398 }
399 }
400
401 true
404}
405
406fn count_features_in_file_filtered(
408 path: &std::path::Path,
409 params: &GetFeatureParams,
410) -> ServiceResult<usize> {
411 let features = load_features_from_file(path)?;
412
413 let filtered = apply_memory_filters(&features, params)?;
415
416 Ok(filtered.len())
417}
418
419async fn generate_geojson_response(
421 state: &WfsState,
422 type_names: &[&str],
423 params: &GetFeatureParams,
424) -> Result<Response, ServiceError> {
425 let mut all_features = Vec::new();
426
427 for type_name in type_names {
428 let ft = state
429 .get_feature_type(type_name.trim())
430 .ok_or_else(|| ServiceError::NotFound(format!("Feature type: {}", type_name)))?;
431
432 let mut features = match &ft.source {
433 FeatureSource::Memory(features) => features.clone(),
434 FeatureSource::File(path) => load_features_from_file(path)?,
435 FeatureSource::Database(_) => {
436 return Err(ServiceError::Internal(
437 "Database sources not yet implemented".to_string(),
438 ));
439 }
440 FeatureSource::DatabaseSource(db_source) => {
441 return Err(ServiceError::Internal(format!(
444 "DatabaseSource feature retrieval not yet implemented for table '{}'. \
445 Use oxigdal-postgis for PostGIS connections.",
446 db_source.table_name
447 )));
448 }
449 };
450
451 if let Some(ref bbox_str) = params.bbox {
453 features = apply_bbox_filter(features, bbox_str)?;
454 }
455
456 if let Some(ref props) = params.property_name {
458 features = filter_properties(features, props)?;
459 }
460
461 if let Some(start) = params.start_index {
463 features = features.into_iter().skip(start).collect();
464 }
465
466 if let Some(max_count) = params.count {
468 features.truncate(max_count);
469 }
470
471 all_features.extend(features);
472 }
473
474 let feature_collection = geojson::FeatureCollection {
475 bbox: None,
476 features: all_features,
477 foreign_members: None,
478 };
479
480 let json = serde_json::to_string_pretty(&feature_collection)
481 .map_err(|e| ServiceError::Serialization(e.to_string()))?;
482
483 Ok(([(header::CONTENT_TYPE, "application/geo+json")], json).into_response())
484}
485
486async fn generate_gml_response(
488 state: &WfsState,
489 type_names: &[&str],
490 params: &GetFeatureParams,
491) -> Result<Response, ServiceError> {
492 let _geojson_response = generate_geojson_response(state, type_names, params).await?;
495
496 let xml = format!(
498 r#"<?xml version="1.0" encoding="UTF-8"?>
499<wfs:FeatureCollection
500 xmlns:wfs="http://www.opengis.net/wfs/2.0"
501 xmlns:gml="http://www.opengis.net/gml/3.2"
502 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
503 timeStamp="{}">
504 <!-- GML encoding would go here -->
505 <!-- For production, use full GML 3.2 encoding -->
506</wfs:FeatureCollection>"#,
507 chrono::Utc::now().to_rfc3339()
508 );
509
510 Ok(([(header::CONTENT_TYPE, "application/gml+xml")], xml).into_response())
511}
512
513fn generate_feature_schema(
515 state: &WfsState,
516 type_names: &[&str],
517) -> Result<Response, ServiceError> {
518 use quick_xml::{
519 Writer,
520 events::{BytesDecl, BytesEnd, BytesStart, Event},
521 };
522 use std::io::Cursor;
523
524 let mut writer = Writer::new(Cursor::new(Vec::new()));
525
526 writer
527 .write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), None)))
528 .map_err(|e| ServiceError::Xml(e.to_string()))?;
529
530 let mut schema = BytesStart::new("xsd:schema");
531 schema.push_attribute(("xmlns:xsd", "http://www.w3.org/2001/XMLSchema"));
532 schema.push_attribute(("xmlns:gml", "http://www.opengis.net/gml/3.2"));
533 schema.push_attribute(("elementFormDefault", "qualified"));
534 schema.push_attribute(("targetNamespace", "http://www.opengis.net/wfs/2.0"));
535
536 writer
537 .write_event(Event::Start(schema))
538 .map_err(|e| ServiceError::Xml(e.to_string()))?;
539
540 let mut import = BytesStart::new("xsd:import");
542 import.push_attribute(("namespace", "http://www.opengis.net/gml/3.2"));
543 import.push_attribute((
544 "schemaLocation",
545 "http://schemas.opengis.net/gml/3.2.1/gml.xsd",
546 ));
547 writer
548 .write_event(Event::Empty(import))
549 .map_err(|e| ServiceError::Xml(e.to_string()))?;
550
551 for type_name in type_names {
552 let _ft = state
553 .get_feature_type(type_name)
554 .ok_or_else(|| ServiceError::NotFound(format!("Feature type: {}", type_name)))?;
555
556 let mut element = BytesStart::new("xsd:element");
558 element.push_attribute(("name", *type_name));
559 element.push_attribute(("type", "gml:AbstractFeatureType"));
560 element.push_attribute(("substitutionGroup", "gml:AbstractFeature"));
561
562 writer
563 .write_event(Event::Empty(element))
564 .map_err(|e| ServiceError::Xml(e.to_string()))?;
565 }
566
567 writer
568 .write_event(Event::End(BytesEnd::new("xsd:schema")))
569 .map_err(|e| ServiceError::Xml(e.to_string()))?;
570
571 let xml = String::from_utf8(writer.into_inner().into_inner())
572 .map_err(|e| ServiceError::Xml(e.to_string()))?;
573
574 Ok(([(header::CONTENT_TYPE, "application/xml")], xml).into_response())
575}
576
577fn load_features_from_file(path: &std::path::Path) -> ServiceResult<Vec<geojson::Feature>> {
579 let contents = std::fs::read_to_string(path)?;
580
581 match path.extension().and_then(|e| e.to_str()) {
582 Some("geojson") | Some("json") => {
583 let geojson: geojson::GeoJson = contents.parse()?;
584 match geojson {
585 geojson::GeoJson::FeatureCollection(fc) => Ok(fc.features),
586 geojson::GeoJson::Feature(f) => Ok(vec![f]),
587 _ => Err(ServiceError::InvalidGeoJson(
588 "Expected FeatureCollection or Feature".to_string(),
589 )),
590 }
591 }
592 _ => Err(ServiceError::UnsupportedFormat(format!(
593 "Unsupported file format: {:?}",
594 path.extension()
595 ))),
596 }
597}
598
599fn apply_bbox_filter(
601 features: Vec<geojson::Feature>,
602 bbox_str: &str,
603) -> ServiceResult<Vec<geojson::Feature>> {
604 let parts: Vec<&str> = bbox_str.split(',').collect();
605 if parts.len() < 4 {
606 return Err(ServiceError::InvalidBbox(
607 "BBOX must have at least 4 coordinates".to_string(),
608 ));
609 }
610
611 let minx: f64 = parts[0]
612 .parse()
613 .map_err(|_| ServiceError::InvalidBbox("Invalid minx".to_string()))?;
614 let miny: f64 = parts[1]
615 .parse()
616 .map_err(|_| ServiceError::InvalidBbox("Invalid miny".to_string()))?;
617 let maxx: f64 = parts[2]
618 .parse()
619 .map_err(|_| ServiceError::InvalidBbox("Invalid maxx".to_string()))?;
620 let maxy: f64 = parts[3]
621 .parse()
622 .map_err(|_| ServiceError::InvalidBbox("Invalid maxy".to_string()))?;
623
624 let filtered: Vec<_> = features
625 .into_iter()
626 .filter(|f| {
627 if let Some(ref _geometry) = f.geometry {
628 if let Some(bbox) = &f.bbox {
629 bbox.len() >= 4
631 && bbox[0] <= maxx
632 && bbox[2] >= minx
633 && bbox[1] <= maxy
634 && bbox[3] >= miny
635 } else {
636 true
638 }
639 } else {
640 false
641 }
642 })
643 .collect();
644
645 Ok(filtered)
646}
647
648fn filter_properties(
650 features: Vec<geojson::Feature>,
651 property_names: &str,
652) -> ServiceResult<Vec<geojson::Feature>> {
653 let names: Vec<&str> = property_names.split(',').map(|s| s.trim()).collect();
654
655 let filtered: Vec<_> = features
656 .into_iter()
657 .map(|mut f| {
658 if let Some(ref mut props) = f.properties {
659 let filtered_props: serde_json::Map<String, serde_json::Value> = props
660 .iter()
661 .filter(|(k, _)| names.contains(&k.as_str()))
662 .map(|(k, v)| (k.clone(), v.clone()))
663 .collect();
664 f.properties = Some(filtered_props);
665 }
666 f
667 })
668 .collect();
669
670 Ok(filtered)
671}
672
673#[cfg(test)]
674mod tests {
675 use super::*;
676 use crate::wfs::{FeatureTypeInfo, ServiceInfo};
677
678 #[tokio::test]
679 async fn test_describe_feature_type() -> Result<(), Box<dyn std::error::Error>> {
680 let info = ServiceInfo {
681 title: "Test WFS".to_string(),
682 abstract_text: None,
683 provider: "COOLJAPAN OU".to_string(),
684 service_url: "http://localhost/wfs".to_string(),
685 versions: vec!["2.0.0".to_string()],
686 };
687
688 let state = WfsState::new(info);
689
690 let ft = FeatureTypeInfo {
691 name: "test_layer".to_string(),
692 title: "Test Layer".to_string(),
693 abstract_text: None,
694 default_crs: "EPSG:4326".to_string(),
695 other_crs: vec![],
696 bbox: None,
697 source: FeatureSource::Memory(vec![]),
698 };
699
700 state.add_feature_type(ft)?;
701
702 let params = serde_json::json!({
703 "TYPENAMES": "test_layer",
704 "OUTPUTFORMAT": "application/xml"
705 });
706
707 let response = handle_describe_feature_type(&state, "2.0.0", ¶ms).await?;
708
709 let (parts, _) = response.into_parts();
710 assert_eq!(
711 parts
712 .headers
713 .get(header::CONTENT_TYPE)
714 .and_then(|h| h.to_str().ok()),
715 Some("application/xml")
716 );
717 Ok(())
718 }
719
720 #[test]
721 fn test_bbox_parsing() {
722 let features = vec![];
723 let result = apply_bbox_filter(features, "-180,-90,180,90");
724 assert!(result.is_ok());
725
726 let result = apply_bbox_filter(vec![], "invalid");
727 assert!(result.is_err());
728 }
729}