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.core.AbstractFeature;
  import org.citygml4j.core.model.core.ImplicitGeometry;
  import org.citygml4j.core.visitor.ObjectWalker;
  import org.citygml4j.tools.log.Logger;
  import org.citygml4j.xml.reader.FeatureInfo;
  import org.xmlobjects.gml.model.geometry.AbstractGeometry;
  import org.xmlobjects.gml.model.geometry.Envelope;
  
  import java.util.HashSet;
  import java.util.Locale;
  import java.util.Set;
  
  public class BoundingBoxFilter {
      private final Logger log = Logger.getInstance();
      private final ReferenceSystemValidator referenceSystemValidator = new ReferenceSystemValidator();
      private final Envelope boundingBox;
      private Mode mode = Mode.INTERSECTS;
      private String rootReferenceSystem;
  
      public enum Mode {
          INTERSECTS,
          WITHIN;
  
          @Override
          public String toString() {
              return name().toLowerCase(Locale.ROOT);
          }
      }
  
      private BoundingBoxFilter(Envelope boundingBox) {
          this.boundingBox = boundingBox;
      }
  
      public static BoundingBoxFilter of(Envelope boundingBox) {
          return new BoundingBoxFilter(boundingBox);
      }
  
      public BoundingBoxFilter withMode(Mode mode) {
          this.mode = mode != null ? mode : Mode.INTERSECTS;
          return this;
      }
  
      public BoundingBoxFilter withRootReferenceSystem(FeatureInfo cityModelInfo) {
          if (cityModelInfo != null
                  && cityModelInfo.getBoundedBy() != null
                  && cityModelInfo.getBoundedBy().isSetEnvelope()) {
              rootReferenceSystem = cityModelInfo.getBoundedBy().getEnvelope().getSrsName();
          }
  
          return this;
      }
  
      public boolean filter(AbstractFeature feature) {
          if (!referenceSystemValidator.validate(feature, rootReferenceSystem)) {
              log.error(CityObjects.getObjectSignature(feature) + ": Cannot apply bounding box filter because the " +
                      "city object uses multiple reference systems.");
              return false;
          }
  
          Envelope candidate = feature.computeEnvelope();
          if (!candidate.isEmpty() && candidate.isValid()) {
              return mode == Mode.WITHIN ?
                      boundingBox.contains(candidate):
                      boundingBox.intersects(candidate);
          }
  
          return false;
      }
  
      private static class ReferenceSystemValidator extends ObjectWalker {
          private final Set<String> referenceSystems = new HashSet<>();
  
          public boolean validate(AbstractFeature feature, String rootReferenceSystem) {
              try {
                  if (rootReferenceSystem != null) {
                      referenceSystems.add(rootReferenceSystem);
                  }
  
                  feature.accept(this);
                  return referenceSystems.size() <= 1;
              } finally {
                  referenceSystems.clear();
              }
          }
  
          @Override
          public void visit(AbstractFeature feature) {
              if (feature.getBoundedBy() != null
                      && feature.getBoundedBy().isSetEnvelope()
                      && feature.getBoundedBy().getEnvelope().getSrsName() != null) {
                  referenceSystems.add(feature.getBoundedBy().getEnvelope().getSrsName());
              }
  
              super.visit(feature);
          }
  
          @Override
          public void visit(AbstractGeometry geometry) {
              if (geometry.getSrsName() != null) {
                  referenceSystems.add(geometry.getSrsName());
              }
  
              super.visit(geometry);
          }
  
          @Override
          public void visit(ImplicitGeometry implicitGeometry) {
              if (implicitGeometry.getReferencePoint() != null
                      && implicitGeometry.getReferencePoint().getObject() != null) {
                  implicitGeometry.getRelativeGeometry().getObject().accept(this);
              }
          }
      }
  }
  