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.*;
  import org.citygml4j.core.visitor.ObjectWalker;
  import org.xmlobjects.gml.model.basictypes.Code;
  import org.xmlobjects.gml.model.geometry.AbstractGeometry;
  import org.xmlobjects.gml.model.geometry.GeometryProperty;
  import org.xmlobjects.gml.util.id.DefaultIdCreator;
  import org.xmlobjects.gml.util.reference.ReferenceResolver;
  
  import java.util.*;
  
  public class GeometryReferenceResolver {
      private final GeometryCopyBuilder copyBuilder = GeometryCopyBuilder.newInstance();
      private final Map<String, GeometryReference> references = new HashMap<>();
      private final ResolverProcessor resolverProcessor = new ResolverProcessor();
      private final GeometryPropertyProcessor geometryPropertyProcessor = new GeometryPropertyProcessor();
      private final GeometryProcessor geometryProcessor = new GeometryProcessor();
  
      private boolean createCityObjectRelations;
      private int resolvedReferencesCounter;
      private int cityObjectRelationsCounter;
  
      private GeometryReferenceResolver() {
      }
  
      public static GeometryReferenceResolver newInstance() {
          return new GeometryReferenceResolver();
      }
  
      public boolean isCreateCityObjectRelations() {
          return createCityObjectRelations;
      }
  
      public GeometryReferenceResolver createCityObjectRelations(boolean createCityObjectRelations) {
          this.createCityObjectRelations = createCityObjectRelations;
          return this;
      }
  
      public void processGeometryReferences(AbstractFeature feature, int featureId, ReferenceResolver referenceResolver) {
          referenceResolver.resolveReferences(feature);
          processGeometryReferences(feature, featureId);
      }
  
      public void processGeometryReferences(AbstractFeature feature, int featureId) {
          geometryPropertyProcessor.process(feature, featureId);
      }
  
      public void processReferencedGeometries(AbstractFeature feature) {
          feature.accept(geometryProcessor);
      }
  
      public int getResolvedReferencesCounter() {
          return resolvedReferencesCounter;
      }
  
      public int getCityObjectRelationsCounter() {
          return cityObjectRelationsCounter;
      }
  
      public boolean hasReferences() {
          return !references.isEmpty();
      }
  
      public void resolveGeometryReferences(AbstractFeature feature, int featureId, ReferenceResolver referenceResolver) {
          referenceResolver.resolveReferences(feature);
          resolveGeometryReferences(feature, featureId);
      }
  
      public void resolveGeometryReferences(AbstractFeature feature, int featureId) {
          if (!references.isEmpty()) {
              resolverProcessor.resolve(feature, featureId);
          }
      }
  
      private class ResolverProcessor extends ObjectWalker {
          private final Map<AbstractCityObject, Integer> childIds = new IdentityHashMap<>();
          private final Map<AbstractCityObject, Set<String>> relatedTos = new IdentityHashMap<>();
          private int featureId;
  
          void resolve(AbstractFeature feature, int featureId) {
              this.featureId = featureId;
              feature.accept(this);
              childIds.clear();
              relatedTos.clear();
          }
  
          @Override
          public void visit(AbstractCityObject cityObject) {
              childIds.put(cityObject, childIds.size());
              super.visit(cityObject);
          }
  
          @Override
          public void visit(AbstractGeometry geometry) {
              if (createCityObjectRelations && geometry.getId() != null) {
                  GeometryReference reference = references.get(geometry.getId());
                  if (reference != null) {
                      AbstractCityObject cityObject = geometry.getParent(AbstractCityObject.class);
                      if (cityObject != null) {
                          cityObject.setId(reference.getOwner());
                          for (String relatedTo : reference.getRelatedTos()) {
                              if (relatedTos.computeIfAbsent(cityObject, v -> new HashSet<>()).add(relatedTo)) {
                                  cityObjectRelationsCounter++;
                                  CityObjectRelation relation = new CityObjectRelation("#" + relatedTo);
                                  relation.setRelationType(new Code("shared"));
                                  cityObject.getRelatedTo().add(new CityObjectRelationProperty(relation));
                              }
                          }
                      }
                  }
              }
          }
  
          @Override
          public void visit(GeometryProperty<?> property) {
              if (property.getObject() == null
                      && property.getHref() != null
                      && property.getParent(ImplicitGeometry.class) == null) {
                  GeometryReference reference = references.get(CityObjects.getIdFromReference(property.getHref()));
                  if (reference != null) {
                      resolvedReferencesCounter++;
                      AbstractCityObject cityObject = property.getParent(AbstractCityObject.class);
                      if (cityObject != null) {
                          String target = reference.getTarget(featureId, childIds.get(cityObject));
                          if (target != null) {
                              AbstractGeometry geometry = reference.createGeometryFor(featureId);
                              property.setInlineObjectIfValid(geometry);
                              property.setHref(null);
  
                              if (createCityObjectRelations
                                      && relatedTos.computeIfAbsent(cityObject, v -> new HashSet<>())
                                      .add(reference.getOwner())) {
                                  cityObjectRelationsCounter++;
                                  cityObject.setId(target);
                                  CityObjectRelation relation = new CityObjectRelation("#" + reference.getOwner());
                                  relation.setRelationType(new Code("shared"));
                                  cityObject.getRelatedTo().add(new CityObjectRelationProperty(relation));
                              }
                          } else {
                              property.setHref("#" + reference.getOrCreateGeometryId(featureId));
                          }
                      }
                  }
              } else if (!property.isSetReferencedObject()) {
                  super.visit(property);
              }
          }
      }
  
      private class GeometryPropertyProcessor extends ObjectWalker {
          private final Map<String, Deque<AbstractCityObject>> referees = new HashMap<>();
          private final Map<AbstractCityObject, Integer> childIds = new IdentityHashMap<>();
  
          void process(AbstractFeature feature, int featureId) {
              feature.accept(this);
  
              for (Map.Entry<String, Deque<AbstractCityObject>> entry : referees.entrySet()) {
                  Deque<AbstractCityObject> candidates = entry.getValue();
                  candidates.stream().filter(AbstractSpaceBoundary.class::isInstance)
                          .map(AbstractSpaceBoundary.class::cast)
                          .map(boundary -> boundary.getParent(AbstractSpace.class))
                          .forEach(space -> candidates.removeIf(space::equals));
  
                  GeometryReference reference = references.computeIfAbsent(entry.getKey(), v -> new GeometryReference());
                  reference.addTarget(candidates.getFirst(), featureId, childIds.get(candidates.getFirst()));
              }
  
              referees.clear();
              childIds.clear();
          }
  
          @Override
          public void visit(AbstractCityObject cityObject) {
              childIds.put(cityObject, childIds.size());
              super.visit(cityObject);
          }
  
          @Override
          public void visit(GeometryProperty<?> property) {
              if (property.getObject() == null
                      && property.getHref() != null
                      && property.getParent(ImplicitGeometry.class) == null) {
                  AbstractCityObject cityObject = property.getParent(AbstractCityObject.class);
                  if (cityObject != null) {
                      String reference = CityObjects.getIdFromReference(property.getHref());
                      referees.computeIfAbsent(reference, v -> new ArrayDeque<>()).add(cityObject);
                  }
              } else {
                  super.visit(property);
              }
          }
      }
  
      private class GeometryProcessor extends ObjectWalker {
  
          @Override
          public void visit(AbstractGeometry geometry) {
              if (geometry.getId() != null) {
                  GeometryReference reference = references.get(geometry.getId());
                  if (reference != null) {
                      reference.setGeometry(geometry);
                      reference.setOwner(geometry.getParent(AbstractCityObject.class));
                  }
              }
          }
      }
  
      private class GeometryReference {
          private AbstractGeometry geometry;
          private String owner;
          private final Map<Integer, String> geometryIds = new HashMap<>();
          private final Map<String, String> targets = new HashMap<>();
  
          AbstractGeometry createGeometryFor(int featureId) {
              AbstractGeometry copy = copyBuilder.copy(geometry);
              copy.setId(getOrCreateGeometryId(featureId));
              return copy;
          }
  
          void setGeometry(AbstractGeometry geometry) {
              this.geometry = geometry;
          }
  
          String getOrCreateGeometryId(int featureId) {
              return geometryIds.computeIfAbsent(featureId, v -> DefaultIdCreator.getInstance().createId());
          }
  
          String getOwner() {
              return owner;
          }
  
          void setOwner(AbstractCityObject owner) {
              this.owner = CityObjects.getOrCreateId(owner);
          }
  
          Collection<String> getRelatedTos() {
              return targets.values();
          }
  
          String getTarget(int featureId, int childId) {
              return targets.get(featureId + "_" + childId);
          }
  
          void addTarget(AbstractCityObject cityObject, int featureId, int childId) {
              targets.put(featureId + "_" + childId, CityObjects.getOrCreateId(cityObject));
          }
      }
  }
  