From 39a8abf6b9ca2371a9dc5d48bef2bfae28370b9e Mon Sep 17 00:00:00 2001 From: Bertil Chapuis Date: Thu, 31 Oct 2024 15:43:29 +0100 Subject: [PATCH] Add a csv data store (#901) * Add a csv datastore * Add tests that parses csv files and a sample geonames file * Format the source code * Fix issues identified by codeql and sonar --- .../baremaps/storage/csv/CsvDataStore.java | 98 +++++++++ .../baremaps/storage/csv/CsvDataTable.java | 201 ++++++++++++++++++ .../storage/csv/CsvDataTableGeonamesTest.java | 126 +++++++++++ .../storage/csv/CsvDataTableTest.java | 195 +++++++++++++++++ .../apache/baremaps/testing/TestFiles.java | 3 + 5 files changed, 623 insertions(+) create mode 100644 baremaps-core/src/main/java/org/apache/baremaps/storage/csv/CsvDataStore.java create mode 100644 baremaps-core/src/main/java/org/apache/baremaps/storage/csv/CsvDataTable.java create mode 100644 baremaps-core/src/test/java/org/apache/baremaps/storage/csv/CsvDataTableGeonamesTest.java create mode 100644 baremaps-core/src/test/java/org/apache/baremaps/storage/csv/CsvDataTableTest.java diff --git a/baremaps-core/src/main/java/org/apache/baremaps/storage/csv/CsvDataStore.java b/baremaps-core/src/main/java/org/apache/baremaps/storage/csv/CsvDataStore.java new file mode 100644 index 000000000..93b0624a9 --- /dev/null +++ b/baremaps-core/src/main/java/org/apache/baremaps/storage/csv/CsvDataStore.java @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you 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.apache.baremaps.storage.csv; + +import java.io.File; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import org.apache.baremaps.data.storage.DataSchema; +import org.apache.baremaps.data.storage.DataStore; +import org.apache.baremaps.data.storage.DataStoreException; +import org.apache.baremaps.data.storage.DataTable; + +/** + * A DataStore implementation that manages a single CSV file. + */ +public class CsvDataStore implements DataStore { + + private final String tableName; + private final DataSchema schema; + private final CsvDataTable dataTable; + + /** + * Constructs a CsvDataStore with the specified table name, schema, and CSV file. + * + * @param tableName the name of the table + * @param schema the data schema defining the structure + * @param csvFile the CSV file to read data from + * @param hasHeader whether the CSV file includes a header row + * @param separator the character used to separate columns in the CSV file + * @throws IOException if an I/O error occurs + */ + public CsvDataStore(String tableName, DataSchema schema, File csvFile, boolean hasHeader, + char separator) throws IOException { + this.tableName = tableName; + this.schema = schema; + this.dataTable = new CsvDataTable(schema, csvFile, hasHeader, separator); + } + + /** + * {@inheritDoc} + */ + @Override + public List list() throws DataStoreException { + return Collections.singletonList(tableName); + } + + /** + * {@inheritDoc} + */ + @Override + public DataTable get(String name) throws DataStoreException { + if (this.tableName.equals(name)) { + return dataTable; + } else { + throw new DataStoreException("Table '" + name + "' not found."); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void add(DataTable table) throws DataStoreException { + throw new UnsupportedOperationException("Adding tables is not supported in CsvDataStore."); + } + + /** + * {@inheritDoc} + */ + @Override + public void add(String name, DataTable table) throws DataStoreException { + throw new UnsupportedOperationException("Adding tables is not supported in CsvDataStore."); + } + + /** + * {@inheritDoc} + */ + @Override + public void remove(String name) throws DataStoreException { + throw new UnsupportedOperationException("Removing tables is not supported in CsvDataStore."); + } +} diff --git a/baremaps-core/src/main/java/org/apache/baremaps/storage/csv/CsvDataTable.java b/baremaps-core/src/main/java/org/apache/baremaps/storage/csv/CsvDataTable.java new file mode 100644 index 000000000..033578a69 --- /dev/null +++ b/baremaps-core/src/main/java/org/apache/baremaps/storage/csv/CsvDataTable.java @@ -0,0 +1,201 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you 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.apache.baremaps.storage.csv; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.dataformat.csv.CsvMapper; +import com.fasterxml.jackson.dataformat.csv.CsvSchema; +import java.io.File; +import java.io.IOException; +import java.util.*; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; +import org.apache.baremaps.data.storage.*; +import org.locationtech.jts.io.WKTReader; + +/** + * A DataTable implementation that reads data from a CSV file using Jackson. + */ +public class CsvDataTable implements DataTable { + + private final DataSchema schema; + private final File csvFile; + private final CsvSchema csvSchema; + private final long size; + + /** + * Constructs a CsvDataTable with the specified schema, CSV file, header presence, and separator. + * + * @param schema the data schema defining the structure + * @param csvFile the CSV file to read data from + * @param hasHeader whether the CSV file includes a header row + * @param separator the character used to separate columns in the CSV file + * @throws IOException if an I/O error occurs + */ + public CsvDataTable(DataSchema schema, File csvFile, boolean hasHeader, char separator) + throws IOException { + this.schema = schema; + this.csvFile = csvFile; + this.csvSchema = buildCsvSchema(schema, hasHeader, separator); + this.size = calculateSize(); + } + + /** + * Builds the CsvSchema for Jackson based on the provided DataSchema, header presence, and + * separator. + * + * @param dataSchema the data schema + * @param hasHeader whether the CSV file includes a header row + * @param separator the character used to separate columns + * @return the CsvSchema for Jackson + */ + private CsvSchema buildCsvSchema(DataSchema dataSchema, boolean hasHeader, char separator) { + CsvSchema.Builder builder = CsvSchema.builder(); + for (DataColumn column : dataSchema.columns()) { + builder.addColumn(column.name()); + } + return builder.setUseHeader(hasHeader).setColumnSeparator(separator).build(); + } + + /** + * Calculates the number of rows in the CSV file. + * + * @return the number of rows + * @throws IOException if an I/O error occurs + */ + private long calculateSize() throws IOException { + try (var parser = new CsvMapper().readerFor(Map.class) + .with(csvSchema) + .createParser(csvFile)) { + long rowCount = 0; + while (parser.nextToken() != null) { + if (parser.currentToken() == JsonToken.START_OBJECT) { + rowCount++; + } + } + return rowCount; + } + } + + @Override + public DataSchema schema() { + return schema; + } + + @Override + public boolean add(DataRow row) { + throw new UnsupportedOperationException("Adding rows is not supported."); + } + + @Override + public void clear() { + throw new UnsupportedOperationException("Clearing rows is not supported."); + } + + @Override + public long size() { + return size; + } + + @Override + public Iterator iterator() { + try { + CsvMapper csvMapper = new CsvMapper(); + JsonParser parser = csvMapper.readerFor(Map.class) + .with(csvSchema) + .createParser(csvFile); + + Iterator> csvIterator = csvMapper.readerFor(Map.class) + .with(csvSchema) + .readValues(parser); + + return new Iterator<>() { + @Override + public boolean hasNext() { + return csvIterator.hasNext(); + } + + @Override + public DataRow next() { + Map csvRow = csvIterator.next(); + DataRow dataRow = schema.createRow(); + + for (int i = 0; i < schema.columns().size(); i++) { + DataColumn column = schema.columns().get(i); + String columnName = column.name(); + String value = csvRow.get(columnName); + + if (value != null) { + Object parsedValue = parseValue(column, value); + dataRow.set(i, parsedValue); + } else { + dataRow.set(i, null); + } + } + return dataRow; + } + }; + + } catch (IOException e) { + throw new DataStoreException("Error reading CSV file", e); + } + } + + /** + * Parses the string value from the CSV according to the column type. + * + * @param column the data column + * @param value the string value from the CSV + * @return the parsed value + */ + private Object parseValue(DataColumn column, String value) { + DataColumn.Type type = column.type(); + try { + if (value == null || value.isEmpty()) { + return null; + } + return switch (type) { + case STRING -> value; + case INTEGER -> Integer.parseInt(value); + case LONG -> Long.parseLong(value); + case FLOAT -> Float.parseFloat(value); + case DOUBLE -> Double.parseDouble(value); + case BOOLEAN -> Boolean.parseBoolean(value); + case GEOMETRY, POINT, LINESTRING, POLYGON, MULTIPOINT, MULTILINESTRING, MULTIPOLYGON, + GEOMETRYCOLLECTION -> { + WKTReader reader = new WKTReader(); + yield reader.read(value); + } + default -> throw new IllegalArgumentException("Unsupported column type: " + type); + }; + } catch (Exception e) { + throw new DataStoreException("Error parsing value for column " + column.name(), e); + } + } + + @Override + public Spliterator spliterator() { + return Spliterators.spliteratorUnknownSize(iterator(), Spliterator.ORDERED); + } + + @Override + public Stream stream() { + return StreamSupport.stream(spliterator(), false); + } +} diff --git a/baremaps-core/src/test/java/org/apache/baremaps/storage/csv/CsvDataTableGeonamesTest.java b/baremaps-core/src/test/java/org/apache/baremaps/storage/csv/CsvDataTableGeonamesTest.java new file mode 100644 index 000000000..5fa787b3e --- /dev/null +++ b/baremaps-core/src/test/java/org/apache/baremaps/storage/csv/CsvDataTableGeonamesTest.java @@ -0,0 +1,126 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you 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.apache.baremaps.storage.csv; + +import static org.apache.baremaps.testing.TestFiles.GEONAMES_CSV; +import static org.junit.jupiter.api.Assertions.*; + +import java.io.IOException; +import java.util.List; +import org.apache.baremaps.data.storage.*; +import org.junit.jupiter.api.*; +import org.locationtech.jts.geom.Point; + +class CsvDataTableGeonamesTest { + + @Test + void testGeonamesCsvDataTable() throws IOException { + List columns = List.of( + new DataColumnFixed("id", DataColumn.Cardinality.REQUIRED, DataColumn.Type.INTEGER), + new DataColumnFixed("name", DataColumn.Cardinality.OPTIONAL, DataColumn.Type.STRING), + new DataColumnFixed("asciiname", DataColumn.Cardinality.OPTIONAL, DataColumn.Type.STRING), + new DataColumnFixed("alternatenames", DataColumn.Cardinality.OPTIONAL, + DataColumn.Type.STRING), + new DataColumnFixed("latitude", DataColumn.Cardinality.OPTIONAL, DataColumn.Type.DOUBLE), + new DataColumnFixed("longitude", DataColumn.Cardinality.OPTIONAL, DataColumn.Type.DOUBLE), + new DataColumnFixed("feature_class", DataColumn.Cardinality.OPTIONAL, + DataColumn.Type.STRING), + new DataColumnFixed("feature_code", DataColumn.Cardinality.OPTIONAL, + DataColumn.Type.STRING), + new DataColumnFixed("country_code", DataColumn.Cardinality.OPTIONAL, + DataColumn.Type.STRING), + new DataColumnFixed("cc2", DataColumn.Cardinality.OPTIONAL, DataColumn.Type.STRING), + new DataColumnFixed("admin1_code", DataColumn.Cardinality.OPTIONAL, DataColumn.Type.STRING), + new DataColumnFixed("admin2_code", DataColumn.Cardinality.OPTIONAL, DataColumn.Type.STRING), + new DataColumnFixed("admin3_code", DataColumn.Cardinality.OPTIONAL, DataColumn.Type.STRING), + new DataColumnFixed("admin4_code", DataColumn.Cardinality.OPTIONAL, DataColumn.Type.STRING), + new DataColumnFixed("population", DataColumn.Cardinality.OPTIONAL, DataColumn.Type.LONG), + new DataColumnFixed("elevation", DataColumn.Cardinality.OPTIONAL, DataColumn.Type.INTEGER), + new DataColumnFixed("dem", DataColumn.Cardinality.OPTIONAL, DataColumn.Type.INTEGER), + new DataColumnFixed("timezone", DataColumn.Cardinality.OPTIONAL, DataColumn.Type.STRING), + new DataColumnFixed("modification_date", DataColumn.Cardinality.OPTIONAL, + DataColumn.Type.STRING)); + DataSchema schema = new DataSchemaImpl("geonames", columns); + + boolean hasHeader = false; + char separator = '\t'; + DataTable dataTable = new CsvDataTable(schema, GEONAMES_CSV.toFile(), hasHeader, separator); + + assertEquals(5, dataTable.size(), "DataTable should have 5 rows."); + + int rowCount = 0; + for (DataRow row : dataTable) { + rowCount++; + + // Extract values + Integer id = (Integer) row.get("id"); + String name = (String) row.get("name"); + Double latitude = (Double) row.get("latitude"); + Double longitude = (Double) row.get("longitude"); + + // Perform assertions for each row + assertNotNull(id, "ID should not be null."); + assertNotNull(name, "Name should not be null."); + assertNotNull(latitude, "Latitude should not be null."); + assertNotNull(longitude, "Longitude should not be null."); + + switch (id) { + case 1: + assertEquals("HEIG", name); + assertEquals(1.111, latitude); + assertEquals(1.111, longitude); + break; + case 2: + assertEquals("Yverdon-les-bains", name); + assertEquals(2.222, latitude); + assertEquals(2.222, longitude); + break; + case 3: + assertEquals("Route de Cheseaux 1", name); + assertEquals(3.333, latitude); + assertEquals(3.333, longitude); + break; + case 4: + assertEquals("Switzerland", name); + assertEquals(4.444, latitude); + assertEquals(4.444, longitude); + break; + case 5: + assertEquals("Switzerland", name); + assertEquals(47.00016, latitude); + assertEquals(8.01427, longitude); + break; + default: + fail("Unexpected ID: " + id); + } + + Point point = createPoint(longitude, latitude); + assertNotNull(point, "Point geometry should not be null."); + } + assertEquals(5, rowCount, "Row count should be 5."); + } + + private Point createPoint(Double longitude, Double latitude) { + if (longitude != null && latitude != null) { + return new org.locationtech.jts.geom.GeometryFactory() + .createPoint(new org.locationtech.jts.geom.Coordinate(longitude, latitude)); + } else { + return null; + } + } +} diff --git a/baremaps-core/src/test/java/org/apache/baremaps/storage/csv/CsvDataTableTest.java b/baremaps-core/src/test/java/org/apache/baremaps/storage/csv/CsvDataTableTest.java new file mode 100644 index 000000000..ede62effb --- /dev/null +++ b/baremaps-core/src/test/java/org/apache/baremaps/storage/csv/CsvDataTableTest.java @@ -0,0 +1,195 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you 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.apache.baremaps.storage.csv; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import org.apache.baremaps.data.storage.*; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.locationtech.jts.geom.Geometry; + +class CsvDataTableTest { + + private File tempCsvFile; + + @BeforeEach + void setUp() throws IOException { + Path tempCsvFilePath = Files.createTempFile("test", ".csv"); + tempCsvFile = tempCsvFilePath.toFile(); + tempCsvFile.deleteOnExit(); + } + + @AfterEach + void tearDown() { + if (tempCsvFile.exists()) { + tempCsvFile.delete(); + } + } + + @Test + void testCsvWithHeaderAndCommaSeparator() throws IOException { + String csvContent = """ + id,name,geom + 1,PointA,"POINT(1 1)" + 2,PointB,"POINT(2 2)" + """; + Files.writeString(tempCsvFile.toPath(), csvContent); + List columns = List.of( + new DataColumnFixed("id", DataColumn.Cardinality.REQUIRED, DataColumn.Type.INTEGER), + new DataColumnFixed("name", DataColumn.Cardinality.OPTIONAL, DataColumn.Type.STRING), + new DataColumnFixed("geom", DataColumn.Cardinality.OPTIONAL, DataColumn.Type.GEOMETRY)); + DataSchema schema = new DataSchemaImpl("test_table", columns); + DataTable dataTable = new CsvDataTable(schema, tempCsvFile, true, ','); + assertEquals(2, dataTable.size()); + int rowCount = 0; + for (DataRow row : dataTable) { + rowCount++; + Integer id = (Integer) row.get("id"); + String name = (String) row.get("name"); + Geometry geometry = (Geometry) row.get("geom"); + assertNotNull(id); + assertNotNull(name); + assertNotNull(geometry); + assertEquals("Point" + (rowCount == 1 ? "A" : "B"), name); + assertEquals("POINT (" + rowCount + " " + rowCount + ")", geometry.toText()); + } + assertEquals(2, rowCount); + } + + @Test + void testCsvWithoutHeaderAndSemicolonSeparator() throws IOException { + String csvContent = """ + 1;PointA;"POINT(1 1)" + 2;PointB;"POINT(2 2)" + """; + Files.writeString(tempCsvFile.toPath(), csvContent); + List columns = List.of( + new DataColumnFixed("column1", DataColumn.Cardinality.REQUIRED, DataColumn.Type.INTEGER), + new DataColumnFixed("column2", DataColumn.Cardinality.OPTIONAL, DataColumn.Type.STRING), + new DataColumnFixed("column3", DataColumn.Cardinality.OPTIONAL, DataColumn.Type.GEOMETRY)); + DataSchema schema = new DataSchemaImpl("test_table", columns); + DataTable dataTable = new CsvDataTable(schema, tempCsvFile, false, ';'); + assertEquals(2, dataTable.size()); + int rowCount = 0; + for (DataRow row : dataTable) { + rowCount++; + Integer id = (Integer) row.get("column1"); + String name = (String) row.get("column2"); + Geometry geometry = (Geometry) row.get("column3"); + + // Verify data + assertNotNull(id); + assertNotNull(name); + assertNotNull(geometry); + + assertEquals("Point" + (rowCount == 1 ? "A" : "B"), name); + assertEquals("POINT (" + rowCount + " " + rowCount + ")", geometry.toText()); + } + assertEquals(2, rowCount); + } + + @Test + void testCsvWithDifferentDataTypes() throws IOException { + String csvContent = """ + int_col,double_col,bool_col,string_col + 1,1.1,true,Hello + 2,2.2,false,World + """; + Files.writeString(tempCsvFile.toPath(), csvContent); + List columns = List.of( + new DataColumnFixed("int_col", DataColumn.Cardinality.REQUIRED, DataColumn.Type.INTEGER), + new DataColumnFixed("double_col", DataColumn.Cardinality.REQUIRED, DataColumn.Type.DOUBLE), + new DataColumnFixed("bool_col", DataColumn.Cardinality.REQUIRED, DataColumn.Type.BOOLEAN), + new DataColumnFixed("string_col", DataColumn.Cardinality.REQUIRED, DataColumn.Type.STRING)); + DataSchema schema = new DataSchemaImpl("test_table", columns); + DataTable dataTable = new CsvDataTable(schema, tempCsvFile, true, ','); + assertEquals(2, dataTable.size()); + int rowCount = 0; + for (DataRow row : dataTable) { + rowCount++; + Integer intCol = (Integer) row.get("int_col"); + Double doubleCol = (Double) row.get("double_col"); + Boolean boolCol = (Boolean) row.get("bool_col"); + String stringCol = (String) row.get("string_col"); + + // Verify data + assertEquals(rowCount, intCol); + assertEquals(rowCount * 1.1, doubleCol); + assertEquals(rowCount == 1, boolCol); + assertEquals(rowCount == 1 ? "Hello" : "World", stringCol); + } + assertEquals(2, rowCount); + } + + @Test + void testCsvWithInvalidData() throws IOException { + String csvContent = """ + id,name + abc,TestName + """; + Files.writeString(tempCsvFile.toPath(), csvContent); + List columns = List.of( + new DataColumnFixed("id", DataColumn.Cardinality.REQUIRED, DataColumn.Type.INTEGER), + new DataColumnFixed("name", DataColumn.Cardinality.OPTIONAL, DataColumn.Type.STRING)); + DataSchema schema = new DataSchemaImpl("test_table", columns); + DataTable dataTable = new CsvDataTable(schema, tempCsvFile, true, ','); + assertThrows(RuntimeException.class, () -> { + for (DataRow row : dataTable) { + // This line should throw an exception because abc is not a valid integer + row.values(); + } + }); + } + + @Test + void testAddAndClearUnsupportedOperations() throws IOException { + String csvContent = ""; + Files.writeString(tempCsvFile.toPath(), csvContent); + List columns = List.of( + new DataColumnFixed("id", DataColumn.Cardinality.REQUIRED, DataColumn.Type.INTEGER), + new DataColumnFixed("name", DataColumn.Cardinality.OPTIONAL, DataColumn.Type.STRING)); + DataSchema schema = new DataSchemaImpl("test_table", columns); + DataTable dataTable = new CsvDataTable(schema, tempCsvFile, true, ','); + assertThrows(UnsupportedOperationException.class, () -> dataTable.add(null)); + assertThrows(UnsupportedOperationException.class, dataTable::clear); + } + + @Test + void testSizeCalculation() throws IOException { + String csvContent = """ + id,name + 1,Name1 + 2,Name2 + 3,Name3 + """; + Files.writeString(tempCsvFile.toPath(), csvContent); + List columns = List.of( + new DataColumnFixed("id", DataColumn.Cardinality.REQUIRED, DataColumn.Type.INTEGER), + new DataColumnFixed("name", DataColumn.Cardinality.OPTIONAL, DataColumn.Type.STRING)); + DataSchema schema = new DataSchemaImpl("test_table", columns); + DataTable dataTable = new CsvDataTable(schema, tempCsvFile, true, ','); + assertEquals(3, dataTable.size()); + } +} diff --git a/baremaps-testing/src/main/java/org/apache/baremaps/testing/TestFiles.java b/baremaps-testing/src/main/java/org/apache/baremaps/testing/TestFiles.java index 4e7e2e2a9..2b284166e 100644 --- a/baremaps-testing/src/main/java/org/apache/baremaps/testing/TestFiles.java +++ b/baremaps-testing/src/main/java/org/apache/baremaps/testing/TestFiles.java @@ -76,6 +76,9 @@ private TestFiles() { public static final Path TILEJSON_JSON = resolve("baremaps-testing/data/tilesets/tilejson.json"); + public static final Path GEONAMES_CSV = + resolve("baremaps-testing/data/geonames/sample.txt"); + /* The geometries of the osm-sample/sample.osm.xml file */ private static final GeometryFactory GEOMETRY_FACTORY = new GeometryFactory();