DataModel.java
/*
* Copyright (C) 2014-2017, 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"})
public 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;
}
}