/* * Copyright (C) 2016-2021, Stichting Mapcode Foundation (http://www.mapcode.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 com.mapcode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; // ---------------------------------------------------------------------------------------------- // Package private implementation class. For internal use within the Mapcode implementation only. // ---------------------------------------------------------------------------------------------- /** * This class contains the module that reads the Mapcode areas into memory and processes them. */ @SuppressWarnings("MagicNumber") class DataModel { private static final Logger LOG = LoggerFactory.getLogger(DataModel.class); // TODO: This class needs a thorough description of what the data file format looks like and what all bit fields means exactly. private static final int HEADER_ID_1 = 0; private static final int HEADER_ID_2 = 1; private static final int HEADER_VERSION_LO = 2; private static final int HEADER_VERSION_HI = 3; private static final int HEADER_NR_TERRITORIES_RECS_LO = 4; private static final int HEADER_NR_TERRITORIES_RECS_HI = 5; private static final int HEADER_NR_TERRITORIES_LO = 6; private static final int HEADER_NR_TERRITORIES_HI = 7; private static final int HEADER_SIZE = HEADER_NR_TERRITORIES_HI + 1; private static final int BYTES_PER_INT = 2; private static final int BYTES_PER_LONG = 4; private static final int POS_DATA_LON_MICRO_DEG_MIN = 0; private static final int POS_DATA_LAT_MICRO_DEG_MIN = 1; private static final int POS_DATA_LON_MICRO_DEG_MAX = 2; private static final int POS_DATA_LAT_MICRO_DEG_MAX = 3; private static final int POS_DATA_DATA_FLAGS = 4; private static final int DATA_FIELDS_PER_REC = 5; private static final int MASK_DATA_DATA_FLAGS = 0xffff; private static final int SHIFT_POS_DATA_SMART_DIV = 16; private static final int POS_INDEX_FIRST_RECORD = 0; private static final int POS_INDEX_LAST_RECORD = 1; private static final String DATA_FILE_NAME = "/com/mapcode/mminfo.dat"; private static final int FILE_BUFFER_SIZE = 50000; private static final int DATA_VERSION_MIN = 220; private static int readIntLoHi(final int lo, final int hi) { return (lo & 0xff) + ((hi & 0xff) << 8); } private static int readLongLoHi(final int lo, final int mid1, final int mid2, final int hi) { return ((lo & 0xff)) + ((mid1 & 0xff) << 8) + ((mid2 & 0xff) << 16) + ((hi & 0xff) << 24); } // Data. private final int nrTerritories; private final int nrTerritoryRecords; private final int[] index; private final int[] data; private static volatile DataModel instance = null; private static final Object mutex = new Object(); @SuppressWarnings({"DoubleCheckedLocking", "SynchronizationOnStaticField"}) static DataModel getInstance() { if (instance == null) { synchronized (mutex) { if (instance == null) { instance = new DataModel(DATA_FILE_NAME); } } } return instance; } @SuppressWarnings("NestedTryStatement") DataModel(@Nonnull final String fileName) throws IncorrectDataModelException { // Read data only once in static initializer. LOG.info("DataModel: reading regions from file: {}", fileName); final byte[] readBuffer = new byte[FILE_BUFFER_SIZE]; int total = 0; try { final InputStream inputStream = DataModel.class.getResourceAsStream(fileName); try { final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); try { // Read the input stream, copy to memory buffer. int nrBytes = inputStream.read(readBuffer); while (nrBytes > 0) { total += nrBytes; outputStream.write(readBuffer, 0, nrBytes); nrBytes = inputStream.read(readBuffer); } // Copy stream into data. final byte[] bytes = outputStream.toByteArray(); assert total == bytes.length; if (total < 12) { LOG.error("DataModel: expected more than {} bytes", total); throw new IncorrectDataModelException("Data file corrupt: " + fileName); } // Read "MC", VERSION. assert total > 8; // "MC" (2) + VERSION (2) + NR TERRITORIES (2) + NR TERRITORY RECORDS (2). if ((bytes[HEADER_ID_1] != 'M') || (bytes[HEADER_ID_2] != 'C')) { throw new IncorrectDataModelException("Data file does not start with correct header: " + fileName); } final int dataVersion = readIntLoHi(bytes[HEADER_VERSION_LO], bytes[HEADER_VERSION_HI]); if (dataVersion < DATA_VERSION_MIN) { throw new IncorrectDataModelException("Data file version " + dataVersion + " too low: " + fileName); } // Read header: NR TERRITORIES, NR RECTANGLE RECORDS. nrTerritoryRecords = readIntLoHi(bytes[HEADER_NR_TERRITORIES_RECS_LO], bytes[HEADER_NR_TERRITORIES_RECS_HI]); nrTerritories = readIntLoHi(bytes[HEADER_NR_TERRITORIES_LO], bytes[HEADER_NR_TERRITORIES_HI]); // Check if the number of territories matches the enumeration in Territory. if (nrTerritories != Territory.values().length) { LOG.error("DataModel: expected {} territories, got {}", Territory.values().length, nrTerritories); throw new IncorrectDataModelException("Data file corrupt: " + fileName); } // Check if the expected file size matched what we found. final int expectedSize = HEADER_SIZE + ((nrTerritories + 1) * BYTES_PER_INT) + (nrTerritoryRecords * (DATA_FIELDS_PER_REC * BYTES_PER_LONG)); if (expectedSize != total) { LOG.error("DataModel: expected {} bytes, got {}", expectedSize, total); throw new IncorrectDataModelException("Data file corrupt: " + fileName); } LOG.debug("DataModel: version={} territories={} territory records={}", dataVersion, nrTerritories, nrTerritoryRecords); // Read DATA+START array (2 bytes per territory, plus closing record). index = new int[nrTerritories + 1]; int i = HEADER_SIZE; for (int k = 0; k <= nrTerritories; k++) { index[k] = readIntLoHi(bytes[i], bytes[i + 1]); i += 2; } // Read territory rectangle data (DATA_FIELDS_PER_REC longs per record). data = new int[nrTerritoryRecords * DATA_FIELDS_PER_REC]; for (int k = 0; k < (nrTerritoryRecords * DATA_FIELDS_PER_REC); k++) { data[k] = readLongLoHi(bytes[i], bytes[i + 1], bytes[i + 2], bytes[i + 3]); i += 4; } } finally { outputStream.close(); } } finally { inputStream.close(); } } catch (final IOException e) { throw new IncorrectDataModelException("Cannot initialize static data structure from: " + fileName + ", exception=" + e); } LOG.info("DataModel: regions initialized, read {} bytes", total); } /** * Get number of territories. * * @return Number of territories. */ int getNrTerritories() { return nrTerritories; } /** * Get number of territory records (rectangles per territory). * * @return Number of rectangles per territory. */ // TODO: Explain what territory records contain exactly. int getNrTerritoryRecords() { return nrTerritoryRecords; } @SuppressWarnings("PointlessArithmeticExpression") // TODO: Explain what this does exactly, why not return a Point or Rectangle? int getLonMicroDegMin(final int territoryRecord) { return data[((territoryRecord * DATA_FIELDS_PER_REC) + POS_DATA_LON_MICRO_DEG_MIN)]; } int getLatMicroDegMin(final int territoryRecord) { return data[(territoryRecord * DATA_FIELDS_PER_REC) + POS_DATA_LAT_MICRO_DEG_MIN]; } int getLonMicroDegMax(final int territoryRecord) { return data[(territoryRecord * DATA_FIELDS_PER_REC) + POS_DATA_LON_MICRO_DEG_MAX]; } int getLatMicroDegMax(final int territoryRecord) { return data[(territoryRecord * DATA_FIELDS_PER_REC) + POS_DATA_LAT_MICRO_DEG_MAX]; } int getDataFlags(final int territoryRecord) { return data[(territoryRecord * DATA_FIELDS_PER_REC) + POS_DATA_DATA_FLAGS] & MASK_DATA_DATA_FLAGS; } // TODO: Explain what a "div" and "smart div" is and how you use, and why you need to use it. int getSmartDiv(final int territoryRecord) { return data[(territoryRecord * DATA_FIELDS_PER_REC) + POS_DATA_DATA_FLAGS] >> SHIFT_POS_DATA_SMART_DIV; } // TODO: Explain what these methods do exactly. // Low-level routines for data access. @SuppressWarnings("PointlessArithmeticExpression") int getDataFirstRecord(final int territoryNumber) { assert (0 <= territoryNumber) && (territoryNumber <= Territory.AAA.getNumber()); return index[territoryNumber + POS_INDEX_FIRST_RECORD]; } int getDataLastRecord(final int territoryNumber) { assert (0 <= territoryNumber) && (territoryNumber <= Territory.AAA.getNumber()); return index[territoryNumber + POS_INDEX_LAST_RECORD] - 1; } }