Autopsy 4.22.1
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 */
19package org.sleuthkit.autopsy.datasourcesummary.datamodel;
20
21import java.io.BufferedReader;
22import java.io.IOException;
23import java.io.InputStream;
24import java.io.InputStreamReader;
25import java.util.ArrayList;
26import java.util.List;
27import java.util.logging.Level;
28import java.util.regex.Matcher;
29import java.util.regex.Pattern;
30import java.util.stream.Stream;
31import org.apache.commons.lang3.StringUtils;
32import org.sleuthkit.autopsy.coreutils.Logger;
33
38class 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-2024 Sleuth Kit Labs. Generated on:
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.