Autopsy  4.17.0
Graphical digital forensics platform for The Sleuth Kit and other tools.
ClosestCityMapper.java
Go to the documentation of this file.
1 /*
2  * Autopsy Forensic Browser
3  *
4  * Copyright 2020 Basis Technology Corp.
5  * Contact: carrier <at> sleuthkit <dot> org
6  *
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  * http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  */
19 package org.sleuthkit.autopsy.datasourcesummary.datamodel;
20 
21 import java.io.BufferedReader;
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.io.InputStreamReader;
25 import java.util.ArrayList;
26 import java.util.List;
27 import java.util.logging.Level;
28 import java.util.regex.Matcher;
29 import java.util.regex.Pattern;
30 import java.util.stream.Stream;
31 import org.apache.commons.lang3.StringUtils;
33 
38 class ClosestCityMapper {
39 
40  // class resource for cities lat/lng taken from https://simplemaps.com/data/world-cities
41  private static final String CITIES_CSV_FILENAME = "worldcities.csv";
42 
43  // index within a csv row of pertinent data
44  private static final int CITY_NAME_IDX = 0;
45  private static final int STATE_NAME_IDX = 7;
46  private static final int COUNTRY_NAME_IDX = 4;
47  private static final int LAT_IDX = 2;
48  private static final int LONG_IDX = 3;
49 
50  // regex for parsing csv value from a row. This assumes values are in quotes and no escape sequence is used. Also performs a trim.
51  private static final Pattern CSV_NAIVE_REGEX = Pattern.compile("\"\\s*(([^\"]+?)?)\\s*\"");
52 
53  // Identifies if cities are in last, first format like "Korea, South"
54  private static final Pattern COUNTRY_WITH_COMMA = Pattern.compile("^\\s*([^,]*)\\s*,\\s*([^,]*)\\s*$");
55 
56  private static final int MAX_IDX = Stream.of(CITY_NAME_IDX, STATE_NAME_IDX, COUNTRY_NAME_IDX, LAT_IDX, LONG_IDX)
57  .max(Integer::compare)
58  .get();
59 
60  // singleton instance of this class
61  private static ClosestCityMapper instance = null;
62 
69  static ClosestCityMapper getInstance() throws IOException {
70  if (instance == null) {
71  instance = new ClosestCityMapper();
72  }
73 
74  return instance;
75  }
76 
77  // data structure housing cities
78  private LatLngMap<CityRecord> latLngMap = null;
79 
80  // the logger
81  private final java.util.logging.Logger logger;
82 
88  private ClosestCityMapper() throws IOException {
89  this(
90  GeolocationSummary.class.getResourceAsStream(CITIES_CSV_FILENAME),
91  Logger.getLogger(ClosestCityMapper.class.getName()));
92  }
93 
102  private ClosestCityMapper(InputStream citiesInputStream, java.util.logging.Logger logger) throws IOException {
103  this.logger = logger;
104  latLngMap = new LatLngMap<CityRecord>(parseCsvLines(citiesInputStream, true));
105  }
106 
114  CityRecord findClosest(CityRecord point) {
115  return latLngMap.findClosest(point);
116  }
117 
125  private Double tryParse(String s) {
126  if (s == null) {
127  return null;
128  }
129 
130  try {
131  return Double.parseDouble(s);
132  } catch (NumberFormatException ex) {
133  return null;
134  }
135  }
136 
145  private String parseCountryName(String orig, int lineNum) {
146  if (StringUtils.isBlank(orig)) {
147  logger.log(Level.WARNING, String.format("No country name determined for line %d.", lineNum));
148  return null;
149  }
150 
151  Matcher m = COUNTRY_WITH_COMMA.matcher(orig);
152  if (m.find()) {
153  return String.format("%s %s", m.group(1), m.group(2));
154  }
155 
156  return orig;
157  }
158 
167  private CityRecord getCsvCityRecord(List<String> csvRow, int lineNum) {
168  if (csvRow == null || csvRow.size() <= MAX_IDX) {
169  logger.log(Level.WARNING, String.format("Row at line number %d is required to have at least %d elements and does not.", lineNum, (MAX_IDX + 1)));
170  return null;
171  }
172 
173  // city is required
174  String cityName = csvRow.get(CITY_NAME_IDX);
175  if (StringUtils.isBlank(cityName)) {
176  logger.log(Level.WARNING, String.format("No city name determined for line %d.", lineNum));
177  return null;
178  }
179 
180  // state and country can be optional
181  String stateName = csvRow.get(STATE_NAME_IDX);
182  String countryName = parseCountryName(csvRow.get(COUNTRY_NAME_IDX), lineNum);
183 
184  Double lattitude = tryParse(csvRow.get(LAT_IDX));
185  if (lattitude == null) {
186  logger.log(Level.WARNING, String.format("No lattitude determined for line %d.", lineNum));
187  return null;
188  }
189 
190  Double longitude = tryParse(csvRow.get(LONG_IDX));
191  if (longitude == null) {
192  logger.log(Level.WARNING, String.format("No longitude determined for line %d.", lineNum));
193  return null;
194  }
195 
196  return new CityRecord(cityName, stateName, countryName, lattitude, longitude);
197  }
198 
206  private List<String> parseCsvLine(String line, int lineNum) {
207  if (line == null || line.length() <= 0) {
208  logger.log(Level.INFO, String.format("Line at %d had no content", lineNum));
209  return null;
210  }
211 
212  List<String> allMatches = new ArrayList<String>();
213  Matcher m = CSV_NAIVE_REGEX.matcher(line);
214  while (m.find()) {
215  allMatches.add(m.group(1));
216  }
217 
218  return allMatches;
219  }
220 
231  private List<CityRecord> parseCsvLines(InputStream csvInputStream, boolean ignoreHeaderRow) throws IOException {
232  List<CityRecord> cityRecords = new ArrayList<>();
233  try (BufferedReader reader = new BufferedReader(new InputStreamReader(csvInputStream, "UTF-8"))) {
234  int lineNum = 1;
235  String line = reader.readLine();
236 
237  if (line != null && ignoreHeaderRow) {
238  line = reader.readLine();
239  lineNum++;
240  }
241 
242  while (line != null) {
243  // read next line
244  List<String> rowElements = parseCsvLine(line, lineNum);
245 
246  if (rowElements != null) {
247  cityRecords.add(getCsvCityRecord(rowElements, lineNum));
248  }
249 
250  line = reader.readLine();
251  lineNum++;
252  }
253  }
254 
255  return cityRecords;
256  }
257 
258 }

Copyright © 2012-2021 Basis Technology. Generated on: Tue Jan 19 2021
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.