code: /*
   * citygml-tools - Collection of tools for processing CityGML files
   * https://github.com/citygml4j/citygml-tools
   *
   * citygml-tools is part of the citygml4j project
   *
   * Copyright 2018-2023 Claus Nagel <claus.nagel@gmail.com>
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *     http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package org.citygml4j.tools.util;
  
  import org.citygml4j.core.model.cityobjectgroup.CityObjectGroup;
  import org.citygml4j.core.model.core.AbstractFeature;
  import org.citygml4j.core.model.core.ImplicitGeometry;
  import org.citygml4j.core.visitor.ObjectWalker;
  import org.citygml4j.tools.option.CounterOption;
  import org.citygml4j.tools.option.IdOption;
  import org.citygml4j.tools.option.TypeNamesOption;
  import org.citygml4j.xml.CityGMLContext;
  import org.citygml4j.xml.module.citygml.CityGMLModules;
  import org.citygml4j.xml.module.citygml.CityObjectGroupModule;
  import org.xmlobjects.gml.model.geometry.AbstractGeometry;
  import org.xmlobjects.gml.model.geometry.GeometryProperty;
  
  import javax.xml.namespace.QName;
  import java.util.Map;
  import java.util.Set;
  import java.util.TreeMap;
  
  public class SubsetFilter {
      private final SkippedFeatureProcessor skippedFeatureProcessor = new SkippedFeatureProcessor();
      private final TemplatesProcessor templatesProcessor = new TemplatesProcessor();
      private final Map<String, Integer> counter = new TreeMap<>();
      private final String TEMPLATE_ASSIGNED = "templateAssigned";
  
      private AppearanceRemover appearanceRemover;
      private CityObjectGroupRemover groupRemover;
      private Map<String, AbstractGeometry> templates;
      private Set<QName> typeNames;
      private Set<String> ids;
      private BoundingBoxFilter boundingBoxFilter;
      private boolean invert;
      private CounterOption counterOption;
      private boolean removeGroupMembers;
  
      private long count;
      private long index;
  
      private SubsetFilter() {
      }
  
      public static SubsetFilter newInstance() {
          return new SubsetFilter();
      }
  
      public SubsetFilter withGlobalObjects(GlobalObjects globalObjects) {
          if (globalObjects != null) {
              appearanceRemover = AppearanceRemover.of(globalObjects.getAppearances());
              groupRemover = CityObjectGroupRemover.of(globalObjects.getCityObjectGroups());
              templates = globalObjects.getTemplateGeometries();
          }
  
          return this;
      }
  
      public SubsetFilter withTypeNamesFilter(TypeNamesOption typeNamesOption, CityGMLContext context) {
          this.typeNames = typeNamesOption != null ? typeNamesOption.getTypeNames(context) : null;
          return this;
      }
  
      public SubsetFilter withIdFilter(IdOption idOption) {
          this.ids = idOption != null ? idOption.getIds() : null;
          return this;
      }
  
      public BoundingBoxFilter getBoundingBoxFilter() {
          return boundingBoxFilter;
      }
  
      public SubsetFilter withBoundingBoxFilter(BoundingBoxFilter boundingBoxFilter) {
          this.boundingBoxFilter = boundingBoxFilter;
          return this;
      }
  
      public SubsetFilter invertFilterCriteria(boolean invert) {
          this.invert = invert;
          return this;
      }
  
      public SubsetFilter withCounterOption(CounterOption counterOption) {
          this.counterOption = counterOption;
          return this;
      }
  
      public SubsetFilter removeGroupMembers(boolean removeGroupMembers) {
          this.removeGroupMembers = removeGroupMembers;
          return this;
      }
  
      public Map<String, Integer> getCounter() {
          return counter;
      }
  
      public boolean filter(AbstractFeature feature, QName name, String prefix) {
          boolean keep = true;
          if (typeNames != null) {
              keep = typeNames.contains(name);
          }
  
          if (keep && ids != null) {
              keep = feature.getId() != null && ids.contains(feature.getId());
          }
  
          if (keep && boundingBoxFilter != null) {
              if (templates != null && !templates.isEmpty()) {
                  templatesProcessor.preprocess(feature);
              }
  
              keep = boundingBoxFilter.filter(feature);
          }
  
          if (invert) {
              keep = !keep;
          }
  
          if (keep && counterOption != null) {
              if (index < counterOption.getStartIndex()) {
                  index++;
                  keep = false;
              } else {
                  count++;
              }
  
              keep = keep && count <= counterOption.getCount();
          }
  
          if (keep) {
              templatesProcessor.postprocess(feature);
              counter.merge(prefix + ":" + name.getLocalPart(), 1, Integer::sum);
          } else {
              feature.accept(skippedFeatureProcessor);
          }
  
          return keep;
      }
  
      public void postprocess() {
          postprocessGroups();
          postprocessImplicitGeometries();
          if (appearanceRemover != null) {
              appearanceRemover.postprocess();
          }
      }
  
      private void postprocessGroups() {
          if (groupRemover != null && groupRemover.hasCityObjectGroups()) {
              QName name = null;
              if (typeNames != null) {
                  for (QName typeName : typeNames) {
                      if ("CityObjectGroup".equals(typeName.getLocalPart())
                              && CityGMLModules.isCityGMLNamespace(typeName.getNamespaceURI())) {
                          name = typeName;
                          break;
                      }
                  }
              }
  
              if (name == null) {
                  name = new QName(CityObjectGroupModule.v3_0.getNamespaceURI(), "CityObjectGroup");
              }
  
              for (CityObjectGroup group : groupRemover.getCityObjectGroups()) {
                  if (group.isSetGroupMembers()) {
                      if (!filter(group, name, "grp")) {
                          group.setGroupMembers(null);
                      }
                  }
              }
  
              groupRemover.postprocess();
          }
      }
  
      private void postprocessImplicitGeometries() {
          if (templates != null && !templates.isEmpty()) {
              for (AbstractGeometry template : templates.values()) {
                  if (!template.getLocalProperties().contains(TEMPLATE_ASSIGNED)) {
                      template.accept(skippedFeatureProcessor);
                  }
              }
          }
      }
  
      private class SkippedFeatureProcessor extends ObjectWalker {
  
          @Override
          public void visit(AbstractFeature feature) {
              if (removeGroupMembers && groupRemover != null) {
                  groupRemover.removeMembers(feature);
              }
          }
  
          @Override
          public void visit(AbstractGeometry geometry) {
              if (appearanceRemover != null) {
                  appearanceRemover.removeTarget(geometry);
              }
          }
  
          @Override
          public void visit(ImplicitGeometry implicitGeometry) {
          }
      }
  
      private class TemplatesProcessor extends ObjectWalker {
          private boolean preprocess;
  
          public void preprocess(AbstractFeature feature) {
              preprocess = true;
              feature.accept(this);
          }
  
          public void postprocess(AbstractFeature feature) {
              preprocess = false;
              feature.accept(this);
          }
  
          @Override
          public void visit(ImplicitGeometry implicitGeometry) {
              GeometryProperty<?> property = implicitGeometry.getRelativeGeometry();
              if (property != null) {
                  if (preprocess) {
                      if (property.getObject() == null && property.getHref() != null) {
                          property.setReferencedObjectIfValid(templates.get(
                                  CityObjects.getIdFromReference(property.getHref())));
                      }
                  } else {
                      if (property.isSetInlineObject() && property.getObject().getId() != null) {
                          AbstractGeometry template = templates.get(property.getObject().getId());
                          if (template.getLocalProperties().contains(TEMPLATE_ASSIGNED)) {
                              property.setInlineObject(null);
                              property.setHref("#" + template.getId());
                          } else {
                              template.getLocalProperties().set(TEMPLATE_ASSIGNED, true);
                          }
                      } else if (property.getHref() != null) {
                          AbstractGeometry template = templates.get(CityObjects.getIdFromReference(property.getHref()));
                          if (!template.getLocalProperties().contains(TEMPLATE_ASSIGNED)) {
                              property.setInlineObjectIfValid(template);
                              property.setHref(null);
                              template.getLocalProperties().set(TEMPLATE_ASSIGNED, true);
                          }
                      }
                  }
              }
          }
      }
  }
  