Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ObjectMapper tests #14

Merged
merged 5 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/main/java/ch/geowerkstatt/lk2dxf/DxfWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ public void writeText(String layerName, String textStyle, String text, String hA
var isDefaultAlignment = hAlignmentValue == 0 && vAlignmentValue == 0;

orientation = convertOrientation(orientation);
text = text.replace("\n", "\\U+000A").replace("\r", "\\U+000D").replace("\t", "\\U+0009");

writeElement(0, "TEXT");
writeElement(5, getNextHandle());
Expand Down
12 changes: 9 additions & 3 deletions src/main/java/ch/geowerkstatt/lk2dxf/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,17 @@ public static void main(String[] args) {
private static void processFiles(LK2DxfOptions options) {
Optional<Geometry> perimeter = options.parsePerimeter();

try (var dxfWriter = new DxfWriter(options.dxfFile(), 3, ObjectMapper.getLayerMappings(), "lk2dxf " + Main.VERSION)) {
ObjectMapper objectMapper;
try {
objectMapper = new ObjectMapper();
} catch (Exception e) {
throw new RuntimeException("Failed to read layer mappings.", e);
}

try (var dxfWriter = new DxfWriter(options.dxfFile(), 3, objectMapper.getLayerMappings(), "lk2dxf " + Main.VERSION)) {
for (String xtfFile : options.xtfFiles()) {
try (XtfStreamReader reader = new XtfStreamReader(new File(xtfFile))) {
ObjectMapper mapper = new ObjectMapper();
Stream<MappedObject> objects = mapper.mapObjects(reader.readObjects());
Stream<MappedObject> objects = objectMapper.mapObjects(reader.readObjects());

if (perimeter.isPresent()) {
objects = objects.filter(o -> perimeter.get().intersects(o.geometry()));
Expand Down
155 changes: 88 additions & 67 deletions src/main/java/ch/geowerkstatt/lk2dxf/mapping/ObjectMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,30 +33,37 @@

public final class ObjectMapper {
private static final String MODELS_RESOURCE = "/models";
private static final List<LayerMapping> LAYER_MAPPINGS;
private static final TransferDescription TRANSFER_DESCRIPTION;
private final List<LayerMapping> layerMappings;
private final TransferDescription transferDescription;

private static final Map<AbstractClassDef<?>, Set<PathElement>> CACHE_REQUIREMENTS = new HashMap<AbstractClassDef<?>, Set<PathElement>>();
private static final List<Mapper> FILTERS;
private final Map<AbstractClassDef<?>, Set<PathElement>> cacheRequirements = new HashMap<>();
private final List<Mapper> filters = new ArrayList<>();

static {
try {
LAYER_MAPPINGS = MappingReader.readMappings();
TRANSFER_DESCRIPTION = getTransferDescription();
FILTERS = analyzeLayerMappings();
} catch (Exception e) {
throw new RuntimeException("Failed to read layer mappings.", e);
}
/**
* Create a new {@link ObjectMapper} with the default mappings.
*/
public ObjectMapper() throws IOException, URISyntaxException, Ili2cException {
this(MappingReader.readMappings());
}

private final Map<String, IomObject> objectCache = new HashMap<>();
private final Set<IomObject> objectsWithUnresolvedRef = new HashSet<>();
/**
* Create a new {@link ObjectMapper} with the given layer mappings.
*
* @param layerMappings The layer mappings to use.
*/
public ObjectMapper(List<LayerMapping> layerMappings) throws IOException, URISyntaxException, Ili2cException {
this.layerMappings = layerMappings;
transferDescription = getTransferDescription(layerMappings);
analyzeLayerMappings();
}

private static List<Mapper> analyzeLayerMappings() {
var mappers = new ArrayList<Mapper>();
for (LayerMapping layerMapping : ObjectMapper.LAYER_MAPPINGS) {
/**
* Analyze the layer mappings and populate the {@link #filters} and {@link #cacheRequirements}.
*/
private void analyzeLayerMappings() {
for (LayerMapping layerMapping : layerMappings) {
for (String objectClass : layerMapping.objectClass()) {
var element = TRANSFER_DESCRIPTION.getElement(objectClass);
var element = transferDescription.getElement(objectClass);
if (element == null) {
throw new IllegalArgumentException("No element found for object with id \"" + objectClass + "\".");
}
Expand All @@ -70,10 +77,10 @@ private static List<Mapper> analyzeLayerMappings() {
for (var baseAttributeName : layerMapping.mapping().keySet()) {
var values = layerMapping.mapping().get(baseAttributeName);
var pathElements = getTranslatedPath(classDef, Arrays.asList(baseAttributeName.split("->")));
analyzeCacheRequirements(pathElements, CACHE_REQUIREMENTS);
analyzeCacheRequirements(pathElements, cacheRequirements);
var type = ((AttributeDef) pathElements.getLast().element).getDomainResolvingAliases();
if (!(type instanceof EnumerationType enumerationType)) {
throw new IllegalArgumentException("Only enumeratino types supported" + baseAttributeName);
throw new IllegalArgumentException("Only enumeration types supported: " + baseAttributeName);
}

var attrFilter = new PathMatcher(pathElements, values.stream().map(v -> getTranslatedEnumValue(enumerationType, v)).toList());
Expand All @@ -84,37 +91,36 @@ private static List<Mapper> analyzeLayerMappings() {
var mapper = switch (layerMapping.output()) {
case SURFACE, LINE -> new Mapper(filter,
layerMapping,
getTranslatedPath(classDef, layerMapping.geometry()),
getAndAnalyzeTranslatedPath(classDef, layerMapping.geometry()),
null,
null,
null,
null);
case TEXT -> new Mapper(filter,
layerMapping,
getTranslatedPath(classDef, layerMapping.geometry()),
getTranslatedPath(classDef, layerMapping.orientation()),
getTranslatedPath(classDef, layerMapping.vAlign()),
getTranslatedPath(classDef, layerMapping.hAlign()),
getTranslatedPath(classDef, layerMapping.text()));
getAndAnalyzeTranslatedPath(classDef, layerMapping.geometry()),
getAndAnalyzeTranslatedPath(classDef, layerMapping.orientation()),
getAndAnalyzeTranslatedPath(classDef, layerMapping.vAlign()),
getAndAnalyzeTranslatedPath(classDef, layerMapping.hAlign()),
getAndAnalyzeTranslatedPath(classDef, layerMapping.text()));
case POINT -> new Mapper(filter,
layerMapping,
getTranslatedPath(classDef, layerMapping.geometry()),
getTranslatedPath(classDef, layerMapping.orientation()),
getAndAnalyzeTranslatedPath(classDef, layerMapping.geometry()),
getAndAnalyzeTranslatedPath(classDef, layerMapping.orientation()),
null,
null,
null);
};
mappers.add(mapper);
filters.add(mapper);
}
}
return mappers;
}

/**
* Get the layer mappings as an immutable list.
*/
public static List<LayerMapping> getLayerMappings() {
return Collections.unmodifiableList(LAYER_MAPPINGS);
public List<LayerMapping> getLayerMappings() {
return Collections.unmodifiableList(layerMappings);
}

private static boolean matchesEnumSubValue(List<String> allowedValues, String attrValue) {
Expand Down Expand Up @@ -180,7 +186,7 @@ private static Value resolve(IomObject iomObject, List<PathElement> path, Map<St
}

/**
* Analyze a path for references that need a referenced object in the cache. Update the {@link #CACHE_REQUIREMENTS} accordingly.
* Analyze a path for references that need a referenced object in the cache. Update the {@link #cacheRequirements} accordingly.
*/
private static void analyzeCacheRequirements(List<PathElement> path, Map<AbstractClassDef<?>, Set<PathElement>> cacheRequirements) {
for (int i = 0; i < path.size(); i++) {
Expand All @@ -197,10 +203,13 @@ private static void analyzeCacheRequirements(List<PathElement> path, Map<Abstrac
}

/**
* Get the translated path elements for the given attribute path. Analyzes the path for cache requirements.
* @see #getTranslatedPath(AbstractClassDef, List)
*/
private static List<PathElement> getTranslatedPath(AbstractClassDef<?> viewable, String basePathElements) {
return getTranslatedPath(viewable, Arrays.asList(basePathElements.split("->")));
private List<PathElement> getAndAnalyzeTranslatedPath(AbstractClassDef<?> viewable, String basePathElements) {
var path = getTranslatedPath(viewable, Arrays.asList(basePathElements.split("->")));
analyzeCacheRequirements(path, cacheRequirements);
return path;
}

/**
Expand Down Expand Up @@ -249,12 +258,13 @@ private static List<PathElement> getTranslatedPath(AbstractClassDef<?> viewable,
private static AbstractLeafElement getTranslatedAttributeOrRole(Viewable<?> viewable, String attributeBaseName) {
for (ExtendableContainer<?> extension : viewable.getExtensions()) {
for (var it = ((Viewable<?>) extension).getAttributesAndRoles2(); it.hasNext();) {
var result = switch (it.next().obj) {
var element = it.next().obj;
var result = switch (element) {
case AttributeDef attributeDef ->
(attributeDef.getTranslationOfOrSame().getName().equals(attributeBaseName) ? attributeDef : null);
case RoleDef roleDef ->
(roleDef.getTranslationOfOrSame().getName().equals(attributeBaseName) ? roleDef : null);
default -> throw new IllegalStateException("Unexpected value: " + it.next().obj);
default -> throw new IllegalStateException("Unexpected value: " + element);
};

if (result != null) {
Expand Down Expand Up @@ -294,8 +304,8 @@ private static String getTranslatedEnumValue(EnumerationType type, String enumer
/**
* Get the {@link TransferDescription} with all models used in the layerMappings.
*/
private static TransferDescription getTransferDescription() throws Ili2cException, IOException, URISyntaxException {
var requiredModels = ObjectMapper.LAYER_MAPPINGS.stream()
private static TransferDescription getTransferDescription(List<LayerMapping> layerMappings) throws Ili2cException, IOException, URISyntaxException {
var requiredModels = layerMappings.stream()
.map(LayerMapping::objectClass)
.flatMap(Collection::stream)
.map(c -> c.substring(0, c.indexOf('.')))
Expand Down Expand Up @@ -341,20 +351,23 @@ private static TransferDescription getTransferDescription() throws Ili2cExceptio
* @return A stream of mapped objects.
*/
public Stream<MappedObject> mapObjects(Stream<IomObject> iomObjects) {
final Map<String, IomObject> objectCache = new HashMap<>();
final Set<IomObject> objectsWithUnresolvedRef = new HashSet<>();

// Combine streams using flatMap instead of concat to process objectsWithRef
// after all objects have been processed by the first stream.
var combinedStream = Stream.<Supplier<Stream<Optional<MappedObject>>>>of(
() -> iomObjects.map(b -> mapObject(b, true)),
() -> objectsWithUnresolvedRef.stream().map(b -> mapObject(b, false))
() -> iomObjects.map(b -> mapObject(b, objectCache, objectsWithUnresolvedRef, true)),
() -> objectsWithUnresolvedRef.stream().map(b -> mapObject(b, objectCache, objectsWithUnresolvedRef, false))
).flatMap(Supplier::get);

return combinedStream
.filter(Optional::isPresent)
.map(Optional::get);
}

private Optional<MappedObject> mapObject(IomObject iomObject, boolean unresolvedReferencesAllowed) {
var element = TRANSFER_DESCRIPTION.getElement(iomObject.getobjecttag());
private Optional<MappedObject> mapObject(IomObject iomObject, Map<String, IomObject> objectCache, Set<IomObject> objectsWithUnresolvedRef, boolean unresolvedReferencesAllowed) {
var element = transferDescription.getElement(iomObject.getobjecttag());
if (element == null) {
System.out.println("No element found for object with id \"" + iomObject.getobjectoid() + "\".");
return Optional.empty();
Expand All @@ -365,7 +378,7 @@ private Optional<MappedObject> mapObject(IomObject iomObject, boolean unresolved
}

// cache part of the object if necessary
var pathElements = CACHE_REQUIREMENTS.get(classDef);
var pathElements = cacheRequirements.get(classDef);
if (pathElements != null) {
IomObject cacheObject = new Iom_jObject(iomObject.getobjecttag(), iomObject.getobjectoid());
for (var pathElement : pathElements) {
Expand All @@ -382,29 +395,37 @@ private Optional<MappedObject> mapObject(IomObject iomObject, boolean unresolved
objectCache.put(iomObject.getobjectoid(), cacheObject);
}

return FILTERS.stream()
.filter(filter -> filter.filter.stream().allMatch((f) ->
switch (f.matches(iomObject, objectCache)) {
case UNRESOLVED_REF -> {
if (unresolvedReferencesAllowed) {
objectsWithUnresolvedRef.add(iomObject);
yield false;
} else {
throw new IllegalStateException("Unresolved reference in object with id \"" + iomObject.getobjectoid() + "\".");
}
}
case MATCH -> true;
case NO_MATCH -> false;
}))
.findFirst()
.map(filter -> new MappedObject(
iomObject.getobjectoid(),
Optional.ofNullable(resolve(iomObject, filter.geometry(), objectCache).getComplexObjects()).map(Collection::iterator).map(Iterator::next).orElse(null),
Optional.ofNullable(resolve(iomObject, filter.orientation(), objectCache).getValue()).map(Double::parseDouble).orElse(null),
resolve(iomObject, filter.vAlign(), objectCache).getValue(),
resolve(iomObject, filter.hAlign(), objectCache).getValue(),
resolve(iomObject, filter.text(), objectCache).getValue(),
filter.mapping()));
mapperLoop:
for (var mapper : filters) {
for (var filter : mapper.filter) {
switch (filter.matches(iomObject, objectCache)) {
case UNRESOLVED_REF -> {
if (unresolvedReferencesAllowed) {
objectsWithUnresolvedRef.add(iomObject);
return Optional.empty();
} else {
throw new IllegalStateException("Unresolved reference in object with id \"" + iomObject.getobjectoid() + "\".");
}
}
case NO_MATCH -> {
continue mapperLoop;
}
default -> { } // MATCH, continue with next filter
}
}
return Optional.of(new MappedObject(
iomObject.getobjectoid(),
Optional.ofNullable(resolve(iomObject, mapper.geometry(), objectCache).getComplexObjects()).map(Collection::iterator).map(Iterator::next).orElse(null),
Optional.ofNullable(resolve(iomObject, mapper.orientation(), objectCache).getValue()).map(Double::parseDouble).orElse(null),
resolve(iomObject, mapper.vAlign(), objectCache).getValue(),
resolve(iomObject, mapper.hAlign(), objectCache).getValue(),
resolve(iomObject, mapper.text(), objectCache).getValue(),
mapper.mapping()));
}

// no match found
System.out.println("No match found for object with id \"" + iomObject.getobjectoid() + "\".");
return Optional.empty();
}

private interface Filter {
Expand Down
9 changes: 9 additions & 0 deletions src/test/java/ch/geowerkstatt/lk2dxf/DxfWriterTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,15 @@ public void writeTextOrientation() throws Exception {
}
}

@Test
public void writeMultiLineText() throws Exception {
try (var dxfWriter = new DxfWriter(testOutputWriter, 3, createTestLayerMappings(), null)) {
stringWriter.getBuffer().setLength(0);
dxfWriter.writeText("Test", "arial", "Multiline Text\r\nNext line with Tab <\t>", "Left", "Base", 90, 1.25, IomObjectHelper.createCoord("0", "0"));
assertEquals("0\nTEXT\n5\n1E\n100\nAcDbEntity\n8\nTest\n100\nAcDbText\n7\narial\n10\n0\n20\n0\n40\n1.25\n1\nMultiline Text\\U+000D\\U+000ANext line with Tab <\\U+0009>\n100\nAcDbText\n", stringWriter.toString());
}
}

private IomObject[] createArcTestSegments(double pointRadius, double midPointRadius, int segmentCount) {
var segments = new IomObject[segmentCount + 1];
segments[0] = IomObjectHelper.createCoord(Double.toString(pointRadius), "0");
Expand Down
Loading