From bb77240f28ef8519e1088001d132499dbe3f7978 Mon Sep 17 00:00:00 2001
From: Anti-Alias <drewlikesramen@gmail.com>
Date: Thu, 10 Feb 2022 04:06:38 -0500
Subject: [PATCH] Add group layer support (#146)

* Adding group layer support

* Added test case and fixed parsing code

* Adding comma

* Adding missing newline

* Merging GID changes

* Merging GID changes

* Cleaning files

* Cleaning files

* Cleaning files
---
 assets/tiled_group_layers.tmx |  64 ++++++++++++++++++
 examples/main.rs              |  10 ++-
 src/layers/group.rs           | 121 ++++++++++++++++++++++++++++++++++
 src/layers/mod.rs             |  12 +++-
 src/map.rs                    |  23 +++++--
 tests/lib.rs                  |  64 ++++++++++++++++--
 6 files changed, 278 insertions(+), 16 deletions(-)
 create mode 100644 assets/tiled_group_layers.tmx
 create mode 100644 src/layers/group.rs

diff --git a/assets/tiled_group_layers.tmx b/assets/tiled_group_layers.tmx
new file mode 100644
index 0000000..b027a4e
--- /dev/null
+++ b/assets/tiled_group_layers.tmx
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<map version="1.5" tiledversion="1.7.0" orientation="orthogonal" renderorder="right-down" width="8" height="8" tilewidth="32" tileheight="32" infinite="0" nextlayerid="10" nextobjectid="1">
+ <tileset firstgid="1" source="tilesheet.tsx"/>
+ <layer id="1" name="tile-1" width="8" height="8">
+  <properties>
+   <property name="key" value="value1"/>
+  </properties>
+  <data encoding="csv">
+6,7,8,0,0,0,0,0,
+20,21,22,0,0,0,0,0,
+34,35,36,0,0,0,0,0,
+0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0
+</data>
+ </layer>
+ <group id="3" name="group-1">
+  <properties>
+   <property name="key" value="value4"/>
+  </properties>
+  <layer id="5" name="tile-2" width="8" height="8">
+   <properties>
+    <property name="key" value="value2"/>
+   </properties>
+   <data encoding="csv">
+0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,
+0,0,0,6,7,8,0,0,
+0,0,0,20,21,22,0,0,
+0,0,0,34,35,36,0,0,
+0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0
+</data>
+  </layer>
+ </group>
+ <group id="6" name="group-2">
+  <properties>
+   <property name="key" value="value5"/>
+  </properties>
+  <group id="8" name="group-3">
+   <properties>
+    <property name="key" value="value6"/>
+   </properties>
+   <layer id="9" name="tile-3" width="8" height="8">
+    <properties>
+     <property name="key" value="value3"/>
+    </properties>
+    <data encoding="csv">
+0,0,0,48,49,50,0,0,
+0,0,0,62,63,64,0,0,
+0,0,0,76,77,78,0,0,
+0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0
+</data>
+   </layer>
+  </group>
+ </group>
+</map>
diff --git a/examples/main.rs b/examples/main.rs
index 4269d8d..5379f50 100644
--- a/examples/main.rs
+++ b/examples/main.rs
@@ -30,11 +30,9 @@ fn main() {
                     println!("Infinite tile layer with {} chunks", data.chunks.len())
                 }
             },
-
             tiled::LayerType::ObjectLayer(layer) => {
                 println!("Object layer with {} objects", layer.data().objects.len())
-            }
-
+            },
             tiled::LayerType::ImageLayer(layer) => {
                 println!(
                     "Image layer with {}",
@@ -44,6 +42,12 @@ fn main() {
                         None => "no image".to_owned(),
                     }
                 )
+            },
+            tiled::LayerType::GroupLayer(layer) => {
+                println!(
+                    "Group layer with {} sublayers",
+                    layer.layers().len()
+                )
             }
         }
     }
diff --git a/src/layers/group.rs b/src/layers/group.rs
new file mode 100644
index 0000000..940de75
--- /dev/null
+++ b/src/layers/group.rs
@@ -0,0 +1,121 @@
+use std::path::Path;
+use std::collections::HashMap;
+
+use crate:: {
+    layers::{LayerData, LayerTag},
+    error::TiledError,
+    properties::{parse_properties, Properties},
+    map::MapTilesetGid,
+    util::*,
+    MapWrapper, Layer, Map
+};
+
+#[derive(Debug, PartialEq, Clone)]
+pub struct GroupLayerData {
+    layers: Vec<LayerData>,
+}
+
+impl GroupLayerData {
+    pub(crate) fn new(
+        parser: &mut impl Iterator<Item = XmlEventResult>,
+        infinite: bool,
+        map_path: &Path,
+        tilesets: &[MapTilesetGid],
+    ) -> Result<(Self, Properties), TiledError> {
+        let mut properties = HashMap::new();
+        let mut layers = Vec::new();
+        parse_tag!(parser, "group", {
+            "layer" => |attrs| {
+                layers.push(LayerData::new(
+                    parser,
+                    attrs,
+                    LayerTag::TileLayer,
+                    infinite,
+                    map_path,
+                    &tilesets,
+                )?);
+                Ok(())
+            },
+            "imagelayer" => |attrs| {
+                layers.push(LayerData::new(
+                    parser,
+                    attrs,
+                    LayerTag::ImageLayer,
+                    infinite,
+                    map_path,
+                    &tilesets,
+                )?);
+                Ok(())
+            },
+            "objectgroup" => |attrs| {
+                layers.push(LayerData::new(
+                    parser,
+                    attrs,
+                    LayerTag::ObjectLayer,
+                    infinite,
+                    map_path,
+                    &tilesets,
+                )?);
+                Ok(())
+            },
+            "group" => |attrs| {
+                layers.push(LayerData::new(
+                    parser,
+                    attrs,
+                    LayerTag::GroupLayer,
+                    infinite,
+                    map_path,
+                    &tilesets,
+                )?);
+                Ok(())
+            },
+            "properties" => |_| {
+                properties = parse_properties(parser)?;
+                Ok(())
+            },
+        });
+        Ok((
+            Self { layers },
+            properties,
+        ))
+    }
+}
+
+pub type GroupLayer<'map> = MapWrapper<'map, GroupLayerData>;
+
+impl<'map> GroupLayer<'map> {
+    pub fn layers(&self) -> GroupLayerIter {
+        GroupLayerIter::new(self.map(), self.data())
+    }
+    pub fn get_layer(&self, index: usize) -> Option<Layer> {
+        self.data().layers.get(index).map(|data| Layer::new(self.map(), data))
+    }
+}
+
+/// An iterator that iterates over all the layers in a group layer, obtained via [`GroupLayer::layers`].
+pub struct GroupLayerIter<'map> {
+    map: &'map Map,
+    group: &'map GroupLayerData,
+    index: usize,
+}
+
+impl<'map> GroupLayerIter<'map> {
+    fn new(map: &'map Map, group: &'map GroupLayerData) -> Self {
+        Self { map, group, index: 0 }
+    }
+}
+
+impl<'map> Iterator for GroupLayerIter<'map> {
+    type Item = Layer<'map>;
+    fn next(&mut self) -> Option<Self::Item> {
+        let layer_data = self.group.layers.get(self.index)?;
+        self.index += 1;
+        Some(Layer::new(self.map, layer_data))
+    }
+}
+
+impl<'map> ExactSizeIterator for GroupLayerIter<'map> {
+    fn len(&self) -> usize {
+        self.group.layers.len() - self.index
+    }
+}
diff --git a/src/layers/mod.rs b/src/layers/mod.rs
index b05004a..272b496 100644
--- a/src/layers/mod.rs
+++ b/src/layers/mod.rs
@@ -10,13 +10,15 @@ mod object;
 pub use object::*;
 mod tile;
 pub use tile::*;
+mod group;
+pub use group::*;
 
 #[derive(Clone, PartialEq, Debug)]
 pub enum LayerDataType {
     TileLayer(TileLayerData),
     ObjectLayer(ObjectLayerData),
     ImageLayer(ImageLayerData),
-    // TODO: Support group layers
+    GroupLayer(GroupLayerData),
 }
 
 #[derive(Clone, Copy)]
@@ -24,6 +26,7 @@ pub(crate) enum LayerTag {
     TileLayer,
     ObjectLayer,
     ImageLayer,
+    GroupLayer,
 }
 
 #[derive(Clone, PartialEq, Debug)]
@@ -85,6 +88,10 @@ impl LayerData {
                 let (ty, properties) = ImageLayerData::new(parser, map_path)?;
                 (LayerDataType::ImageLayer(ty), properties)
             }
+            LayerTag::GroupLayer => {
+                let (ty, properties) = GroupLayerData::new(parser, infinite, map_path, tilesets)?;
+                (LayerDataType::GroupLayer(ty), properties)
+            }
         };
 
         Ok(Self {
@@ -116,7 +123,7 @@ pub enum LayerType<'map> {
     TileLayer(TileLayer<'map>),
     ObjectLayer(ObjectLayer<'map>),
     ImageLayer(ImageLayer<'map>),
-    // TODO: Support group layers
+    GroupLayer(GroupLayer<'map>),
 }
 
 impl<'map> LayerType<'map> {
@@ -125,6 +132,7 @@ impl<'map> LayerType<'map> {
             LayerDataType::TileLayer(data) => Self::TileLayer(TileLayer::new(map, data)),
             LayerDataType::ObjectLayer(data) => Self::ObjectLayer(ObjectLayer::new(map, data)),
             LayerDataType::ImageLayer(data) => Self::ImageLayer(ImageLayer::new(map, data)),
+            LayerDataType::GroupLayer(data) => Self::GroupLayer(GroupLayer::new(map, data)),
         }
     }
 }
diff --git a/src/map.rs b/src/map.rs
index dddb7d2..9497470 100644
--- a/src/map.rs
+++ b/src/map.rs
@@ -103,8 +103,8 @@ impl Map {
     }
 
     /// Get an iterator over all the layers in the map in ascending order of their layer index.
-    pub fn layers(&self) -> LayerIter {
-        LayerIter::new(self)
+    pub fn layers(&self) -> MapLayerIter {
+        MapLayerIter::new(self)
     }
 
     /// Returns the layer that has the specified index, if it exists.
@@ -114,18 +114,18 @@ impl Map {
 }
 
 /// An iterator that iterates over all the layers in a map, obtained via [`Map::layers`].
-pub struct LayerIter<'map> {
+pub struct MapLayerIter<'map> {
     map: &'map Map,
     index: usize,
 }
 
-impl<'map> LayerIter<'map> {
+impl<'map> MapLayerIter<'map> {
     fn new(map: &'map Map) -> Self {
         Self { map, index: 0 }
     }
 }
 
-impl<'map> Iterator for LayerIter<'map> {
+impl<'map> Iterator for MapLayerIter<'map> {
     type Item = Layer<'map>;
 
     fn next(&mut self) -> Option<Self::Item> {
@@ -135,7 +135,7 @@ impl<'map> Iterator for LayerIter<'map> {
     }
 }
 
-impl<'map> ExactSizeIterator for LayerIter<'map> {
+impl<'map> ExactSizeIterator for MapLayerIter<'map> {
     fn len(&self) -> usize {
         self.map.layers.len() - self.index
     }
@@ -222,6 +222,17 @@ impl Map {
                 )?);
                 Ok(())
             },
+            "group" => |attrs| {
+                layers.push(LayerData::new(
+                    parser,
+                    attrs,
+                    LayerTag::GroupLayer,
+                    infinite,
+                    map_path,
+                    &tilesets,
+                )?);
+                Ok(())
+            },
             "properties" => |_| {
                 properties = parse_properties(parser)?;
                 Ok(())
diff --git a/tests/lib.rs b/tests/lib.rs
index 396e01c..c43bf8c 100644
--- a/tests/lib.rs
+++ b/tests/lib.rs
@@ -1,7 +1,7 @@
 use std::path::PathBuf;
 use tiled::{
     Color, FilesystemResourceCache, FiniteTileLayerData, Layer, LayerDataType, LayerType, Map,
-    ObjectLayer, PropertyValue, ResourceCache, TileLayer, TileLayerData,
+    ObjectLayer, PropertyValue, ResourceCache, TileLayer, TileLayerData, GroupLayer,
 };
 
 fn as_tile_layer<'map>(layer: Layer<'map>) -> TileLayer<'map> {
@@ -25,6 +25,13 @@ fn as_object_layer<'map>(layer: Layer<'map>) -> ObjectLayer<'map> {
     }
 }
 
+fn as_group_layer<'map>(layer: Layer<'map>) -> GroupLayer<'map> {
+    match layer.layer_type() {
+        LayerType::GroupLayer(x) => x,
+        _ => panic!("Not a group layer"),
+    }
+}
+
 fn compare_everything_but_tileset_sources(r: &Map, e: &Map) {
     assert_eq!(r.version, e.version);
     assert_eq!(r.orientation, e.orientation);
@@ -196,10 +203,10 @@ fn test_object_group_property() {
     let mut cache = FilesystemResourceCache::new();
 
     let r = Map::parse_file("assets/tiled_object_groups.tmx", &mut cache).unwrap();
-    let prop_value: bool = if let Some(&PropertyValue::BoolValue(ref v)) = r
-        .layers()
-        .nth(1)
-        .unwrap()
+    let group_layer = r.get_layer(1).unwrap();
+    let group_layer = as_group_layer(group_layer);
+    let sub_layer = group_layer.get_layer(0).unwrap();
+    let prop_value: bool = if let Some(&PropertyValue::BoolValue(ref v)) = sub_layer
         .data()
         .properties
         .get("an object group property")
@@ -338,3 +345,50 @@ fn test_tint_color() {
         })
     );
 }
+
+#[test]
+fn test_group_layers() {
+    let mut cache = FilesystemResourceCache::new();
+
+    let r = Map::parse_file("assets/tiled_group_layers.tmx", &mut cache).unwrap();
+
+    // Depth = 0
+    let layer_tile_1 = r.get_layer(0).unwrap();
+    let layer_group_1 = r.get_layer(1).unwrap();
+    let layer_group_2 = r.get_layer(2).unwrap();
+
+    assert_eq!(
+        Some(&PropertyValue::StringValue("value1".to_string())),
+        layer_tile_1.data().properties.get("key")
+    );
+    assert_eq!(
+        Some(&PropertyValue::StringValue("value4".to_string())),
+        layer_group_1.data().properties.get("key")
+    );
+    assert_eq!(
+        Some(&PropertyValue::StringValue("value5".to_string())),
+        layer_group_2.data().properties.get("key")
+    );
+
+    // Depth = 1
+    let layer_group_1 = as_group_layer(layer_group_1);
+    let layer_tile_2 = layer_group_1.get_layer(0).unwrap();
+    let layer_group_2 = as_group_layer(layer_group_2);
+    let layer_group_3 = layer_group_2.get_layer(0).unwrap();
+    assert_eq!(
+        Some(&PropertyValue::StringValue("value2".to_string())),
+        layer_tile_2.data().properties.get("key")
+    );
+    assert_eq!(
+        Some(&PropertyValue::StringValue("value6".to_string())),
+        layer_group_3.data().properties.get("key")
+    );
+
+    // Depth = 2
+    let layer_group_3 = as_group_layer(layer_group_3);
+    let layer_tile_3 = layer_group_3.get_layer(0).unwrap();
+    assert_eq!(
+        Some(&PropertyValue::StringValue("value3".to_string())),
+        layer_tile_3.data().properties.get("key")
+    );
+}
-- 
GitLab