Autopsy  4.20.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-2021 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 
70  static ClosestCityMapper getInstance() throws IOException {
71  if (instance == null) {
72  instance = new ClosestCityMapper();
73  }
74 
75  return instance;
76  }
77 
78  // data structure housing cities
79  private LatLngMap<CityRecord> latLngMap = null;
80 
81  // the logger
82  private final java.util.logging.Logger logger;
83 
89  private ClosestCityMapper() throws IOException {
90  this(
91  GeolocationSummary.class.getResourceAsStream(CITIES_CSV_FILENAME),
92  Logger.getLogger(ClosestCityMapper.class.getName()));
93  }
94 
104  private ClosestCityMapper(InputStream citiesInputStream, java.util.logging.Logger logger) throws IOException {
105  this.logger = logger;
106  latLngMap = new LatLngMap<CityRecord>(parseCsvLines(citiesInputStream, true));
107  }
108 
117  CityRecord findClosest(CityRecord point) {
118  return latLngMap.findClosest(point);
119  }
120 
129  private Double tryParse(String s) {
130  if (s == null) {
131  return null;
132  }
133 
134  try {
135  return Double.parseDouble(s);
136  } catch (NumberFormatException ex) {
137  return null;
138  }
139  }
140 
150  private String parseCountryName(String orig, int lineNum) {
151  if (StringUtils.isBlank(orig)) {
152  logger.log(Level.WARNING, String.format("No country name determined for line %d.", lineNum));
153  return null;
154  }
155 
156  Matcher m = COUNTRY_WITH_COMMA.matcher(orig);
157  if (m.find()) {
158  return String.format("%s %s", m.group(1), m.group(2));
159  }
160 
161  return orig;
162  }
163 
173  private CityRecord getCsvCityRecord(List<String> csvRow, int lineNum) {
174  if (csvRow == null || csvRow.size() <= MAX_IDX) {
175  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)));
176  return null;
177  }
178 
179  // city is required
180  String cityName = csvRow.get(CITY_NAME_IDX);
181  if (StringUtils.isBlank(cityName)) {
182  logger.log(Level.WARNING, String.format("No city name determined for line %d.", lineNum));
183  return null;
184  }
185 
186  // state and country can be optional
187  String stateName = csvRow.get(STATE_NAME_IDX);
188  String countryName = parseCountryName(csvRow.get(COUNTRY_NAME_IDX), lineNum);
189 
190  Double lattitude = tryParse(csvRow.get(LAT_IDX));
191  if (lattitude == null) {
192  logger.log(Level.WARNING, String.format("No lattitude determined for line %d.", lineNum));
193  return null;
194  }
195 
196  Double longitude = tryParse(csvRow.get(LONG_IDX));
197  if (longitude == null) {
198  logger.log(Level.WARNING, String.format("No longitude determined for line %d.", lineNum));
199  return null;
200  }
201 
202  return new CityRecord(cityName, stateName, countryName, lattitude, longitude);
203  }
204 
213  private List<String> parseCsvLine(String line, int lineNum) {
214  if (line == null || line.length() <= 0) {
215  logger.log(Level.INFO, String.format("Line at %d had no content", lineNum));
216  return null;
217  }
218 
219  List<String> allMatches = new ArrayList<String>();
220  Matcher m = CSV_NAIVE_REGEX.matcher(line);
221  while (m.find()) {
222  allMatches.add(m.group(1));
223  }
224 
225  return allMatches;
226  }
227 
240  private List<CityRecord> parseCsvLines(InputStream csvInputStream, boolean ignoreHeaderRow) throws IOException {
241  List<CityRecord> cityRecords = new ArrayList<>();
242  try (BufferedReader reader = new BufferedReader(new InputStreamReader(csvInputStream, "UTF-8"))) {
243  int lineNum = 1;
244  String line = reader.readLine();
245 
246  if (line != null && ignoreHeaderRow) {
247  line = reader.readLine();
248  lineNum++;
249  }
250 
251  while (line != null) {
252  // read next line
253  List<String> rowElements = parseCsvLine(line, lineNum);
254 
255  if (rowElements != null) {
256  cityRecords.add(getCsvCityRecord(rowElements, lineNum));
257  }
258 
259  line = reader.readLine();
260  lineNum++;
261  }
262  }
263 
264  return cityRecords;
265  }
266 
267 }

Copyright © 2012-2022 Basis Technology. Generated on: Tue Aug 1 2023
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.