reversejp 0.2.1

ReverseJP is a Rust library for reverse geocoding in Japan.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
#![doc = include_str!("../README.md")]

use std::collections::HashMap;
use std::error::Error;
use std::io::Read;
use zip::read::ZipArchive;

use geometry_rs::{Point, Polygon};
use serde::{Deserialize, Serialize};

// Embedded ZIP files
const EMBEDDED_CLASS10S_DATA: &[u8] = include_bytes!("../data/class10s.json.zip");
const EMBEDDED_LANDSLIDES_0_DATA: &[u8] = include_bytes!("../data/landslides_0.json.zip");
const EMBEDDED_LANDSLIDES_1_DATA: &[u8] = include_bytes!("../data/landslides_1.json.zip");
const EMBEDDED_LANDSLIDES_2_DATA: &[u8] = include_bytes!("../data/landslides_2.json.zip");
const EMBEDDED_LANDSLIDES_3_DATA: &[u8] = include_bytes!("../data/landslides_3.json.zip");
const EMBEDDED_LANDSLIDES_4_DATA: &[u8] = include_bytes!("../data/landslides_4.json.zip");
const EMBEDDED_LANDSLIDES_5_DATA: &[u8] = include_bytes!("../data/landslides_5.json.zip");
const EMBEDDED_LANDSLIDES_6_DATA: &[u8] = include_bytes!("../data/landslides_6.json.zip");
const EMBEDDED_LANDSLIDES_7_DATA: &[u8] = include_bytes!("../data/landslides_7.json.zip");
const EMBEDDED_LANDSLIDES_8_DATA: &[u8] = include_bytes!("../data/landslides_8.json.zip");
const EMBEDDED_LANDSLIDES_9_DATA: &[u8] = include_bytes!("../data/landslides_9.json.zip");

// Function to extract JSON from zip data
fn extract_json_from_zip(
    zip_data: &[u8],
    filename: &str,
) -> Result<String, Box<dyn std::error::Error>> {
    let cursor = std::io::Cursor::new(zip_data);
    let mut archive = ZipArchive::new(cursor)?;
    let mut file = archive.by_name(filename)?;

    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

// Helper function to get class10s data
pub fn get_class10s_data() -> Result<String, Box<dyn std::error::Error>> {
    extract_json_from_zip(EMBEDDED_CLASS10S_DATA, "class10s.json")
}

// Helper function to get landslide data for a specific index
pub fn get_landslide_data(idx: usize) -> Result<String, Box<dyn std::error::Error>> {
    let zip_data: &[u8] = match idx {
        0 => EMBEDDED_LANDSLIDES_0_DATA,
        1 => EMBEDDED_LANDSLIDES_1_DATA,
        2 => EMBEDDED_LANDSLIDES_2_DATA,
        3 => EMBEDDED_LANDSLIDES_3_DATA,
        4 => EMBEDDED_LANDSLIDES_4_DATA,
        5 => EMBEDDED_LANDSLIDES_5_DATA,
        6 => EMBEDDED_LANDSLIDES_6_DATA,
        7 => EMBEDDED_LANDSLIDES_7_DATA,
        8 => EMBEDDED_LANDSLIDES_8_DATA,
        9 => EMBEDDED_LANDSLIDES_9_DATA,
        _ => return Err("Invalid landslide index".into()),
    };

    extract_json_from_zip(zip_data, &format!("landslides_{}.json", idx))
}

// GeoJSON types for deserialization
#[derive(Debug, Deserialize, Serialize)]
pub struct FeatureCollection {
    #[serde(rename = "type")]
    pub feature_type: String,
    pub features: Vec<Feature>,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct Feature {
    #[serde(rename = "type")]
    pub feature_type: String,
    pub geometry: Geometry,
    pub properties: Properties,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct Geometry {
    #[serde(rename = "type")]
    pub geometry_type: String,
    pub coordinates: Vec<Vec<Vec<[f64; 2]>>>,
}

#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Properties {
    pub code: String,
    pub name: String,
    #[serde(rename = "enName", default)]
    pub en_name: String,
}

// Main struct for reverse geocoding
pub struct ReverseJp {
    polygons: Vec<(Polygon, Properties)>,
}

impl Default for ReverseJp {
    fn default() -> Self {
        Self::new()
    }
}

impl ReverseJp {
    /// Create a new instance with no data
    pub fn new() -> Self {
        ReverseJp {
            polygons: Vec::new(),
        }
    }

    /// Create a new instance with embedded GeoJSON data
    ///
    /// This is the recommended way to use the library as it doesn't require
    /// downloading and managing external data files.
    pub fn with_embedded_data() -> Result<Self, Box<dyn Error>> {
        let mut reverse_jp = Self::new();

        // Load embedded data
        reverse_jp.load_from_str(get_class10s_data()?.as_str())?;
        reverse_jp.load_from_str(get_landslide_data(0)?.as_str())?;
        reverse_jp.load_from_str(get_landslide_data(1)?.as_str())?;
        reverse_jp.load_from_str(get_landslide_data(2)?.as_str())?;
        reverse_jp.load_from_str(get_landslide_data(3)?.as_str())?;
        reverse_jp.load_from_str(get_landslide_data(4)?.as_str())?;
        reverse_jp.load_from_str(get_landslide_data(5)?.as_str())?;
        reverse_jp.load_from_str(get_landslide_data(6)?.as_str())?;
        reverse_jp.load_from_str(get_landslide_data(7)?.as_str())?;
        reverse_jp.load_from_str(get_landslide_data(8)?.as_str())?;
        reverse_jp.load_from_str(get_landslide_data(9)?.as_str())?;

        Ok(reverse_jp)
    }

    /// Load data from a GeoJSON string
    fn load_from_str(&mut self, json_str: &str) -> Result<(), Box<dyn Error>> {
        let feature_collection: FeatureCollection = serde_json::from_str(json_str)?;
        self.process_feature_collection(feature_collection)
    }

    // Process a feature collection by converting GeoJSON to polygons
    fn process_feature_collection(
        &mut self,
        feature_collection: FeatureCollection,
    ) -> Result<(), Box<dyn Error>> {
        for feature in feature_collection.features {
            if feature.geometry.geometry_type == "MultiPolygon" {
                for multi_polygon in &feature.geometry.coordinates {
                    for polygon_coords in multi_polygon {
                        // Convert GeoJSON coordinates to geometry-rs points
                        let points: Vec<Point> = polygon_coords
                            .iter()
                            .map(|coord| Point {
                                x: coord[0],
                                y: coord[1],
                            })
                            .collect();

                        // Create geometry-rs polygon
                        let polygon = Polygon::new(points, vec![]);
                        self.polygons.push((polygon, feature.properties.clone()));
                    }
                }
            }
        }

        Ok(())
    }

    /// Find all properties for a given longitude/latitude coordinate
    ///
    /// This method returns all properties (regions) that contain the specified point.
    ///
    /// # Arguments
    ///
    /// * `longitude` - The longitude coordinate
    /// * `latitude` - The latitude coordinate
    ///
    /// # Returns
    ///
    /// A vector of Properties for all regions containing the point
    pub fn find_properties(&self, longitude: f64, latitude: f64) -> Vec<Properties> {
        for lng_shift in [0.0, 0.001, -0.001, 0.002, -0.002, 0.005, -0.005] {
            for lat_shift in [0.0, 0.001, -0.001, 0.002, -0.002, 0.005, -0.005] {
                let point = Point {
                    x: longitude + lng_shift,
                    y: latitude + lat_shift,
                };

                // Find all polygons that contain the point
                let properties: Vec<Properties> = self
                    .polygons
                    .iter()
                    .filter(|(polygon, _)| polygon.contains_point(point))
                    .map(|(_, props)| props.clone())
                    .collect();
                if !properties.is_empty() {
                    return properties;
                }
            }
        }
        vec![]
    }

    /// Find all properties for a given longitude/latitude coordinate, return as hashmap
    ///
    /// This method returns all properties (regions) that contain the specified point,
    /// organized in a HashMap with region codes as keys.
    ///
    /// # Arguments
    ///
    /// * `longitude` - The longitude coordinate
    /// * `latitude` - The latitude coordinate
    ///
    /// # Returns
    ///
    /// A HashMap with region codes as keys and Properties as values
    pub fn find_properties_as_hashmap(
        &self,
        longitude: f64,
        latitude: f64,
    ) -> HashMap<String, Properties> {
        let results: Vec<Properties> = self.find_properties(longitude, latitude);
        let mut map = HashMap::new();

        for props in results {
            map.insert(props.code.clone(), props);
        }

        map
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    use lazy_static::lazy_static;
    use rand::random_range;

    // Initialize JP_CITIES only once for better performance
    lazy_static! {
        static ref JP_CITIES: Vec<&'static cities_json::City> = {
            cities_json::CITIES
                .iter()
                .filter(|city| city.country == "JP")
                .collect()
        };
    }

    /// Utility function to get a random Japanese city from the cities-json crate.
    fn get_random_jp_city() -> Option<&'static cities_json::City> {
        if JP_CITIES.is_empty() {
            return None;
        }

        // Get a random index
        let random_index = random_range(0..JP_CITIES.len());

        // Return the random city
        Some(JP_CITIES[random_index])
    }

    #[test]
    fn test_get_random_jp_city() {
        let city = get_random_jp_city();
        assert!(
            city.is_some(),
            "Should be able to get a random Japanese city"
        );
        if let Some(city) = city {
            assert_eq!(city.country, "JP", "City should be in Japan");
            println!(
                "Random JP city: {} at ({}, {})",
                city.name, city.lat, city.lng
            );
        }
    }

    #[test]
    fn test_polygon_contains_point() {
        let poly = Polygon::new(
            vec![
                Point {
                    x: 90.48826291293898,
                    y: 45.951129815858565,
                },
                Point {
                    x: 90.48826291293898,
                    y: 27.99437617512571,
                },
                Point {
                    x: 122.83201291294,
                    y: 27.99437617512571,
                },
                Point {
                    x: 122.83201291294,
                    y: 45.951129815858565,
                },
                Point {
                    x: 90.48826291293898,
                    y: 45.951129815858565,
                },
            ],
            vec![],
        );

        let p_out = Point {
            x: 130.74216916294148,
            y: 37.649011392900306,
        };

        let p_in = Point {
            x: 99.9804504129416,
            y: 39.70716466970461,
        };

        assert!(!poly.contains_point(p_out));
        assert!(poly.contains_point(p_in));
    }

    #[test]
    fn test_new_reverse_jp() {
        let reverse_jp = ReverseJp::new();
        assert_eq!(reverse_jp.polygons.len(), 0);
    }

    #[test]
    fn test_find_properties_empty() {
        let reverse_jp = ReverseJp::new();
        let properties = reverse_jp.find_properties(139.7670, 35.6812);
        assert_eq!(properties.len(), 0);
    }

    #[test]
    fn test_find_properties_as_hashmap_empty() {
        let reverse_jp = ReverseJp::new();
        let properties = reverse_jp.find_properties_as_hashmap(139.7670, 35.6812);
        assert_eq!(properties.len(), 0);
    }

    #[test]
    fn test_with_embedded_data() {
        let reverse_jp = ReverseJp::with_embedded_data().unwrap();
        assert!(!reverse_jp.polygons.is_empty());

        // Test Tokyo coordinates
        let properties = reverse_jp.find_properties(139.7670, 35.6812);
        assert!(!properties.is_empty());

        // Check if we can find Tokyo
        let found_tokyo = properties
            .iter()
            .any(|p| p.name == "東京都" || p.en_name == "Tokyo");
        assert!(found_tokyo);
    }

    #[test]
    fn test_all_jp_cities_included() {
        // Get all Japanese cities from the cities-json crate
        let jp_cities: Vec<&cities_json::City> = cities_json::CITIES
            .iter()
            .filter(|city| city.country == "JP")
            .collect();

        // Ensure we have a non-zero number of Japanese cities
        assert!(
            !jp_cities.is_empty(),
            "No Japanese cities found in the cities-json crate"
        );
        println!("Found {} Japanese cities to test", jp_cities.len());

        // Create a new ReverseJp instance with embedded data
        let reverse_jp = ReverseJp::with_embedded_data().unwrap();

        // Test each Japanese city
        let mut found_count = 0;
        let mut missing_cities = Vec::new();

        for city in &jp_cities {
            let properties = reverse_jp.find_properties(city.lng, city.lat);
            if !properties.is_empty() {
                found_count += 1;
            } else {
                missing_cities.push(format!("{} ({},{})", city.name, city.lng, city.lat));
            }
        }

        // Print results
        println!(
            "Found geographical data for {}/{} Japanese cities",
            found_count,
            jp_cities.len()
        );

        if !missing_cities.is_empty() {
            println!("Missing cities: {}", missing_cities.join(", "));
        }

        // Assert that all Japanese cities are found (or a high percentage)
        let coverage_percentage = (found_count as f64 / jp_cities.len() as f64) * 100.0;
        assert!(
            coverage_percentage > 90.0,
            "Only {:.2}% of Japanese cities are covered, expected at least 90%",
            coverage_percentage
        );
    }
}