Skip to content

Commit ed2f61d

Browse files
[Junie]: feat: add location input form with autocomplete suggestions (#6)
* feat: add location input form with autocomplete suggestions A form for user location input with autocomplete suggestions was added, linking to a new Java backend city search endpoint. The endpoint returns the top 10 city suggestions, and a submit button is included but currently does nothing. * add cities.json, remove reference to other repo --------- Co-authored-by: jetbrains-junie[bot] <201638009+jetbrains-junie[bot]@users.noreply.github.com> Co-authored-by: Ivan Šarić <invoices@path-variable.com>
1 parent c1a8ead commit ed2f61d

5 files changed

Lines changed: 846179 additions & 0 deletions

File tree

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.example.weatherapp.city;
2+
3+
import com.fasterxml.jackson.annotation.JsonAlias;
4+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
5+
import com.fasterxml.jackson.annotation.JsonProperty;
6+
7+
@JsonIgnoreProperties(ignoreUnknown = true)
8+
public class City {
9+
10+
private String name;
11+
// two-letter country code expected
12+
private String country;
13+
14+
private Double lat;
15+
16+
@JsonAlias({"lng", "lon"})
17+
@JsonProperty("lon")
18+
private Double lon;
19+
20+
// Optional field in some datasets (ignored for formatting)
21+
private String state;
22+
23+
public City() {}
24+
25+
public String getName() { return name; }
26+
public void setName(String name) { this.name = name; }
27+
28+
public String getCountry() { return country; }
29+
public void setCountry(String country) { this.country = country; }
30+
31+
public Double getLat() { return lat; }
32+
public void setLat(Double lat) { this.lat = lat; }
33+
34+
public Double getLon() { return lon; }
35+
public void setLon(Double lon) { this.lon = lon; }
36+
37+
public String getState() { return state; }
38+
public void setState(String state) { this.state = state; }
39+
40+
public String toSuggestionString() {
41+
String n = name != null ? name : "";
42+
String c = country != null ? country : "";
43+
String la = lat != null ? String.valueOf(trimDouble(lat)) : "";
44+
String lo = lon != null ? String.valueOf(trimDouble(lon)) : "";
45+
return String.format("%s %s (%s,%s)", n, c, la, lo).trim();
46+
}
47+
48+
private static double trimDouble(Double d) {
49+
// limit to 6 decimal places in string form
50+
return Math.round(d * 1_000_000d) / 1_000_000d;
51+
}
52+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package com.example.weatherapp.city;
2+
3+
import com.fasterxml.jackson.core.type.TypeReference;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import org.springframework.core.io.ClassPathResource;
6+
import org.springframework.stereotype.Service;
7+
8+
import java.io.IOException;
9+
import java.io.InputStream;
10+
import java.util.Collections;
11+
import java.util.List;
12+
import java.util.Locale;
13+
import java.util.Objects;
14+
import java.util.stream.Collectors;
15+
16+
@Service
17+
public class CitySearchService {
18+
19+
private static final String CLASSPATH_JSON = "cities.json";
20+
21+
private final Object lock = new Object();
22+
private volatile List<City> cached;
23+
24+
private final ObjectMapper mapper = new ObjectMapper();
25+
26+
public List<String> searchSuggestions(String query, int limit) {
27+
if (query == null) return Collections.emptyList();
28+
String q = query.trim().toLowerCase(Locale.ROOT);
29+
if (q.isEmpty()) return Collections.emptyList();
30+
31+
List<City> all = loadAllCities();
32+
if (all.isEmpty()) return Collections.emptyList();
33+
34+
// Simple case-insensitive substring match against name and optionally state
35+
List<City> matched = all.stream()
36+
.filter(c -> {
37+
String name = safeLower(c.getName());
38+
String state = safeLower(c.getState());
39+
return (name != null && name.contains(q)) || (state != null && state.contains(q));
40+
})
41+
.limit(Math.max(limit, 10) * 10L) // scan a bit more then trim to allow for simple ordering
42+
.collect(Collectors.toList());
43+
44+
// Basic ordering: starts-with first, then by name length, then lexicographically
45+
matched.sort((a, b) -> {
46+
String an = safeLower(a.getName());
47+
String bn = safeLower(b.getName());
48+
int as = startsWithScore(an, q) - startsWithScore(bn, q);
49+
if (as != 0) return -as; // higher score first
50+
int al = an != null ? an.length() : Integer.MAX_VALUE;
51+
int bl = bn != null ? bn.length() : Integer.MAX_VALUE;
52+
int cmp = Integer.compare(al, bl);
53+
if (cmp != 0) return cmp;
54+
return Objects.compare(an, bn, String::compareTo);
55+
});
56+
57+
return matched.stream()
58+
.map(City::toSuggestionString)
59+
.distinct()
60+
.limit(limit)
61+
.collect(Collectors.toList());
62+
}
63+
64+
private static int startsWithScore(String s, String q) {
65+
if (s == null) return 0;
66+
return s.startsWith(q) ? 1 : 0;
67+
}
68+
69+
private static String safeLower(String s) {
70+
return s == null ? null : s.toLowerCase(Locale.ROOT);
71+
}
72+
73+
private List<City> loadAllCities() {
74+
List<City> local = cached;
75+
if (local != null) return local;
76+
synchronized (lock) {
77+
if (cached != null) return cached;
78+
List<City> loaded = tryLoadFromClasspath();
79+
cached = loaded;
80+
return cached;
81+
}
82+
}
83+
84+
private List<City> tryLoadFromClasspath() {
85+
try {
86+
ClassPathResource resource = new ClassPathResource(CLASSPATH_JSON);
87+
if (!resource.exists()) return Collections.emptyList();
88+
try (InputStream is = resource.getInputStream()) {
89+
return mapper.readValue(is, new TypeReference<List<City>>() {});
90+
}
91+
} catch (IOException e) {
92+
return Collections.emptyList();
93+
}
94+
}
95+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.example.weatherapp.web;
2+
3+
import com.example.weatherapp.city.CitySearchService;
4+
import org.springframework.http.MediaType;
5+
import org.springframework.web.bind.annotation.GetMapping;
6+
import org.springframework.web.bind.annotation.RequestParam;
7+
import org.springframework.web.bind.annotation.RestController;
8+
9+
import java.util.List;
10+
11+
@RestController
12+
public class CitySearchController {
13+
14+
private final CitySearchService service;
15+
16+
public CitySearchController(CitySearchService service) {
17+
this.service = service;
18+
}
19+
20+
@GetMapping(value = "/api/cities/search", produces = MediaType.APPLICATION_JSON_VALUE)
21+
public List<String> search(@RequestParam(name = "q", required = false) String query,
22+
@RequestParam(name = "limit", required = false, defaultValue = "10") int limit) {
23+
int safeLimit = Math.max(1, Math.min(limit, 50));
24+
return service.searchSuggestions(query, safeLimit);
25+
}
26+
}

0 commit comments

Comments
 (0)