Add support for location plugins (#772)
* Cherry pick location refactor * Refactor :data:openstreetmaps to :data:locations * contract, plugin sdk * Implement serialization, module tweaks * Include check for out-of-date departures in SearchableItemVM.requestUpdatedSearchable() * settings for location plugins * Try not to be too lazy * more fiddling with the plugin SDK * add departures in MapView with mock data for debug builds * change icons * add boats * animate departure lazycolumn * Add MarqueeText for text overflow handling * Define height for Departures in case there is no map to display * Don't inclure railway / highway tags for OSM since there will be location plugins for that * sort by time * - Apply pre-merge changes to LocationSettings - Add banner warning about slowed down location search for large search radii * ditch `showLocationOnMap` * LocationItem: make `showOpeningSchedule` toggleable * LocationItem: make Navigation AssistChip work for people that don't have google maps installed * LocationItem: resolve TODOs * MapTiles: ditch unused code, animate userIndicator * Reintroduce departure list * Add LineColor * Add osm tag `stars` as `userRating` https://taginfo.openstreetmap.org/keys/stars#overview * typealias -> import * Don't add Navigation Chip when there is no way to resolve navigation intents * Add settings migration * Set plugin SDK version to 1.2.0-SNAPSHOT * Deduplicate shared plugin classes, use kotlinx.serialization * Fix imports * Use ZonedDateTime for depature times * Add more line types * Rewrite location serialization * Replace street/houseNumber with address * Add attribution field * Add plugin config * Reject location search requests without lat lon parameters * Add default values to plugin location class * Don't crash if column value is null * Add docs comments to LocationCategory values * Refactor OpeningSchedule as polymorphic * remove dead corpse *ahem* code * Split LocationCategory into category and icon (Also update to Kotlin 2.0, please don't do this at home) * Add more location icons * Fix (?) location deserializer * Add more location icons * More icons * Meh * Add Pub * Disable Github Maven repo if credentials are missing * Add location search specific settings to plugin details screen * Add language parameter * Unbreak the build * Refactor plugin SDK (with breaking changes) * Set plugin SDK version to 2.0.0-SNAPSHOT * Document SDK breaking changes * Implement LocationProvider.getQuery * Add a typesafe cursor API * Oops I did it again next time maybe check if the code is actually compiling before pushing * Add missing return statement * Fix list serialization * Departure time UI adjustment * Use typesafe cursor for weather plugins * Add userRatingCount and emailAddress fields * grrr * Rename and extend LineTypes * Add default lineType to Departure to fix serialization errors * Fix refreshing stored plugin locations * Adapt line name column width to available departures * Fix plugin settings screen category overlap * add LocationItem.GenericTransit * Fix crash during deserialization of locations * Update SDK docs * Replace plugin "official" mark with "verified developer" mark before anyone gets sued * show 'now' when departure is in less than one minute * Add typesafe Bundle API * Implement plugin API changes * Plugin SDK: Fix refresh result not being returned * Update docs * apply alpha to departures that have departured * better (maybe): reduce saturation instead of alpha * Add default values for Attribution * Display attribution * Rearrange location result layout * Reduce searchable update interval to 1 minute * Pass last update time to refresh function * Change refresh path and ensure that timestamp is only update when the item was updated * categorize osm location * Update docs * Optimize location search - run providers in parallel - flatten code * add experimental address parsing for OSM * add poi_category_townhall * Fix popup closing when favorites items are updated * Revert "Fix popup closing when favorites items are updated" This reverts commit fc517fd066c7f8109b6d6df2d4f536af66398207. * Fork AndroidAddressFormatter to `:libs:address-formatter` * migrate `:libs:address-formatter` dependencies to version catalog and update them * also consider addr:{suburb,hamlet} for `Address.city` if city tag is missing * Move poi strings back to strings.xml * Update Jetpack Compose * Move address-formatter back to its original package, add license and readme * Move address-formatter back to its original package --------- Co-authored-by: MM20 <15646950+MM2-0@users.noreply.github.com>
This commit is contained in:
21
libs/address-formatter/LICENSE
Normal file
21
libs/address-formatter/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 woheller69
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
2
libs/address-formatter/Readme.md
Normal file
2
libs/address-formatter/Readme.md
Normal file
@@ -0,0 +1,2 @@
|
||||
Fork of https://github.com/woheller69/AndroidAddressFormatter by woheller69, licensed under the MIT
|
||||
License.
|
||||
30
libs/address-formatter/build.gradle
Normal file
30
libs/address-formatter/build.gradle
Normal file
@@ -0,0 +1,30 @@
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
|
||||
compileSdk libs.versions.compileSdk.get().toInteger()
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
defaultConfig {
|
||||
minSdk libs.versions.minSdk.get().toInteger()
|
||||
versionCode 10
|
||||
versionName "1.0"
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
namespace "de.mm20.launcher2.lib.addressformatter"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation libs.jackson.core
|
||||
implementation libs.jackson.dataformat.yaml
|
||||
implementation libs.guava
|
||||
implementation libs.compiler
|
||||
}
|
||||
17
libs/address-formatter/proguard-rules.pro
vendored
Normal file
17
libs/address-formatter/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in /home/yonjuni/Android/Sdk/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
@@ -0,0 +1,478 @@
|
||||
package org.woheller69.AndroidAddressFormatter;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.type.TypeFactory;
|
||||
import com.fasterxml.jackson.databind.type.MapType;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.github.mustachejava.*;
|
||||
import com.google.common.primitives.Ints;
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.io.StringReader;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.HashMap;
|
||||
import java.util.Spliterator;
|
||||
import java.util.Spliterators;
|
||||
import java.util.function.Function;
|
||||
import com.google.common.base.CaseFormat;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.StreamSupport;
|
||||
import java.util.Optional;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class OsmAddressFormatter {
|
||||
|
||||
private static final RegexPatternCache regexPatternCache = new RegexPatternCache();
|
||||
private static final List<String> knownComponents = OsmAddressFormatter.getKnownComponents();
|
||||
private static final Map<String, String> replacements = new HashMap<String, String>() {{
|
||||
put("[\\},\\s]+$", "");
|
||||
put("^[,\\s]+", "");
|
||||
put("^- ", "");
|
||||
put(",\\s*,", ", ");
|
||||
put("[ \t]+,[ \t]+", ", ");
|
||||
put("[ \t][ \t]+", " ");
|
||||
put("[ \t]\n", "\n");
|
||||
put("\n,", "\n");
|
||||
put(",+", ",");
|
||||
put(",\n", "\n");
|
||||
put("\n[ \t]+", "\n");
|
||||
put("\n+", "\n");
|
||||
}};
|
||||
|
||||
private final ObjectMapper yamlReader = new ObjectMapper(new YAMLFactory());
|
||||
private final boolean abbreviate;
|
||||
private final boolean appendCountry;
|
||||
private final boolean appendUnknown;
|
||||
|
||||
public OsmAddressFormatter(Boolean abbreviate, Boolean appendCountry, Boolean appendUnknown) {
|
||||
this.abbreviate = abbreviate;
|
||||
this.appendCountry = appendCountry;
|
||||
this.appendUnknown = appendUnknown;
|
||||
}
|
||||
|
||||
public String format(String json) throws IOException {
|
||||
return format(json, null);
|
||||
}
|
||||
|
||||
public String format(String json, String fallbackCountryCode) throws IOException {
|
||||
TypeFactory factory = TypeFactory.defaultInstance();
|
||||
MapType type = factory.constructMapType(HashMap.class, String.class, String.class);
|
||||
Map<String, Object> components = null;
|
||||
|
||||
try {
|
||||
components = yamlReader.readValue(json, type);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw(new IOException("Json processing exception", e));
|
||||
}
|
||||
components = normalizeFields(components);
|
||||
|
||||
if (fallbackCountryCode != null) {
|
||||
components.put("country_code", fallbackCountryCode);
|
||||
}
|
||||
|
||||
components = determineCountryCode(components, fallbackCountryCode);
|
||||
String countryCode = components.get("country_code").toString();
|
||||
|
||||
if (appendCountry) {
|
||||
if (Templates.COUNTRY_NAMES.getData().has(countryCode) && components.get("country") == null) {
|
||||
components.put("country", Templates.COUNTRY_NAMES.getData().get(countryCode).asText());
|
||||
}
|
||||
}else components.remove("country"); //if !appendCountry do not show it, even if it is there
|
||||
|
||||
components = applyAliases(components);
|
||||
JsonNode template = findTemplate(components);
|
||||
components = cleanupInput(components, template.get("replace"));
|
||||
return renderTemplate(template, components);
|
||||
}
|
||||
|
||||
Map<String, Object> normalizeFields(Map<String, Object> components) {
|
||||
Map<String, Object> normalizedComponents = new HashMap<>();
|
||||
for (Map.Entry<String, Object> entry : components.entrySet()) {
|
||||
String field = entry.getKey();
|
||||
Object value = entry.getValue();
|
||||
String newField = CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, field);
|
||||
if (!normalizedComponents.containsKey(newField)) {
|
||||
normalizedComponents.put(newField, value);
|
||||
}
|
||||
}
|
||||
return normalizedComponents;
|
||||
}
|
||||
|
||||
Map<String, Object> determineCountryCode(Map<String, Object> components,
|
||||
String fallbackCountryCode) {
|
||||
String countryCode;
|
||||
|
||||
if (components.get("country_code") != null) {
|
||||
countryCode = (String) components.get("country_code");
|
||||
} else if (fallbackCountryCode != null) {
|
||||
countryCode = fallbackCountryCode;
|
||||
} else {
|
||||
throw new Error("No country code provided. Use fallbackCountryCode?");
|
||||
}
|
||||
|
||||
countryCode = countryCode.toUpperCase();
|
||||
|
||||
if (!Templates.WORLDWIDE.getData().has(countryCode) || countryCode.length() != 2) {
|
||||
throw new Error("Invalid country code");
|
||||
}
|
||||
|
||||
if (countryCode.equals("UK")) {
|
||||
countryCode = "GB";
|
||||
}
|
||||
|
||||
JsonNode country = Templates.WORLDWIDE.getData().get(countryCode);
|
||||
if (country != null && country.has("use_country")) {
|
||||
String oldCountryCode = countryCode;
|
||||
countryCode = country.get("use_country").asText().toUpperCase();
|
||||
|
||||
if (country.has("change_country")) {
|
||||
String newCountry = country.get("change_country").asText();
|
||||
Pattern p = regexPatternCache.get("\\$(\\w*)");
|
||||
Matcher m = p.matcher(newCountry);
|
||||
String match;
|
||||
if (m.find()) {
|
||||
match = m.group(1); // $state
|
||||
Pattern p2 = regexPatternCache.get(String.format("\\$%s", match));
|
||||
Matcher m2;
|
||||
if (components.get(match) != null && components.containsKey(match)) {
|
||||
m2 = p2.matcher(newCountry);
|
||||
String toReplace = components.get(match).toString();
|
||||
newCountry = m2.replaceAll(toReplace);
|
||||
} else {
|
||||
m2 = p2.matcher(newCountry);
|
||||
newCountry = m2.replaceAll("");
|
||||
}
|
||||
components.put("country", newCountry);
|
||||
}
|
||||
components.put("country", newCountry);
|
||||
}
|
||||
|
||||
JsonNode oldCountry = Templates.WORLDWIDE.getData().get(oldCountryCode);
|
||||
JsonNode oldCountryAddComponent = oldCountry.get("add_component");
|
||||
if (oldCountryAddComponent != null && oldCountryAddComponent.toString().contains("=")) {
|
||||
String[] pairs = oldCountryAddComponent.textValue().split("=");
|
||||
if (pairs[0].equals("state")) {
|
||||
components.put("state", pairs[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String state = (components.get("state") != null) ? components.get("state").toString() : null;
|
||||
|
||||
if (countryCode.equals("NL") && state != null) {
|
||||
Pattern p1 = regexPatternCache.get("sint maarten", Pattern.CASE_INSENSITIVE);
|
||||
Matcher m1 = p1.matcher(state);
|
||||
Pattern p2 = regexPatternCache.get("aruba", Pattern.CASE_INSENSITIVE);
|
||||
Matcher m2 = p2.matcher(state);
|
||||
if (state.equals("Curaçao")) {
|
||||
countryCode = "CW";
|
||||
components.put("country", "Curaçao");
|
||||
} else if (m1.find()) {
|
||||
countryCode = "SX";
|
||||
components.put("country", "Sint Maarten");
|
||||
} else if (m2.find()) {
|
||||
countryCode = "AW";
|
||||
components.put("country", "Aruba");
|
||||
}
|
||||
}
|
||||
|
||||
components.put("country_code", countryCode);
|
||||
return components;
|
||||
}
|
||||
|
||||
Map<String, Object> cleanupInput(Map<String, Object> components, JsonNode replacements) {
|
||||
Object country = components.get("country");
|
||||
Object state = components.get("state");
|
||||
|
||||
if (country != null && state != null && Ints.tryParse((String) country) != null) {
|
||||
components.put("country", state);
|
||||
components.remove("state");
|
||||
}
|
||||
if (replacements != null && replacements.size() > 0) {
|
||||
for (String component : components.keySet()) {
|
||||
Iterator<JsonNode> rIterator = replacements.iterator();
|
||||
String regex = String.format("^%s=", component);
|
||||
Pattern p = regexPatternCache.get(regex);
|
||||
while (rIterator.hasNext()) {
|
||||
ArrayNode replacement = (ArrayNode) rIterator.next();
|
||||
Matcher m = p.matcher(replacement.get(0).asText());
|
||||
if (m.find()) {
|
||||
m.reset();
|
||||
String value = m.replaceAll("");
|
||||
if (components.get(component).toString().equals(value)) {
|
||||
components.put(component, replacement.get(1).asText());
|
||||
}
|
||||
m.reset();
|
||||
} else {
|
||||
Pattern p2 = regexPatternCache.get(replacement.get(0).asText());
|
||||
Matcher m2 = p2.matcher(components.get(component).toString());
|
||||
String value = m2.replaceAll(replacement.get(1).asText());
|
||||
m.reset();
|
||||
components.put(component, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!components.containsKey("state_code") && components.containsKey("state")) {
|
||||
String stateCode = getStateCode(components.get("state").toString(), components.get("country_code").toString());
|
||||
components.put("state_code", stateCode);
|
||||
Pattern p = regexPatternCache.get("^washington,? d\\.?c\\.?");
|
||||
Matcher m = p.matcher(components.get("state").toString());
|
||||
if (m.find()) {
|
||||
components.put("state_code", "DC");
|
||||
components.put("state", "District of Columbia");
|
||||
components.put("city", "Washington");
|
||||
}
|
||||
}
|
||||
|
||||
if (!components.containsKey("county_code") && components.containsKey("county")) {
|
||||
String countyCode = getCountyCode(components.get("county").toString(), components.get("country_code").toString());
|
||||
components.put("county_code", countyCode);
|
||||
}
|
||||
|
||||
List<String> unknownComponents = components.entrySet().stream().filter(component -> {
|
||||
if (component.getKey() == null) {
|
||||
return false;
|
||||
}
|
||||
return !knownComponents.contains(component.getKey());
|
||||
}).map(component -> component.getValue().toString()).collect(Collectors.toList());
|
||||
|
||||
if (appendUnknown && unknownComponents.size() > 0) {
|
||||
components.put("attention", String.join(", ", unknownComponents));
|
||||
}
|
||||
|
||||
|
||||
if (components.containsKey("postcode")) {
|
||||
String postCode = components.get("postcode").toString();
|
||||
components.put("postcode", postCode);
|
||||
Pattern p1 = regexPatternCache.get("^(\\d{5}),\\d{5}");
|
||||
Pattern p2 = regexPatternCache.get("\\d+;\\d+");
|
||||
Matcher m1 = p1.matcher(postCode);
|
||||
Matcher m2 = p2.matcher(postCode);
|
||||
if (postCode.length() > 20) {
|
||||
components.remove("postcode");
|
||||
} else if (m2.matches()) {
|
||||
components.remove("postcode");
|
||||
} else if (m1.matches()) {
|
||||
components.put("postcode", m1.group(1));
|
||||
}
|
||||
}
|
||||
|
||||
if (abbreviate && components.containsKey("country_code") && Templates.COUNTRY_2_LANG.getData().has(components.get("country_code").toString())) {
|
||||
JsonNode languages = Templates.COUNTRY_2_LANG.getData().get(components.get("country_code").toString());
|
||||
StreamSupport.stream(languages.spliterator(), false)
|
||||
.filter(language -> Templates.ABBREVIATIONS.getData().has(language.textValue()))
|
||||
.map(language -> Templates.ABBREVIATIONS.getData().get(language.textValue())).forEach(
|
||||
languageAbbreviations -> StreamSupport.stream(languageAbbreviations.spliterator(), false)
|
||||
.filter(abbreviation -> abbreviation.has("component"))
|
||||
.forEach(abbreviation -> StreamSupport.stream(abbreviation.get("replacements").spliterator(), false)
|
||||
.forEach(replacement -> {
|
||||
String key = abbreviation.get("component").asText();
|
||||
if (key == null) {
|
||||
return;
|
||||
}
|
||||
if (components.get(key) == null) {
|
||||
return;
|
||||
}
|
||||
String oldComponent = components.get(key).toString();
|
||||
String regex = String.format("\\b%s\\b", replacement.get("src").asText());
|
||||
Pattern p = regexPatternCache.get(regex);
|
||||
Matcher m = p.matcher(oldComponent);
|
||||
String newComponent = m.replaceAll(replacement.get("dest").asText());
|
||||
components.put(key, newComponent);
|
||||
})));
|
||||
}
|
||||
|
||||
Pattern p = regexPatternCache.get("^https?://");
|
||||
return components.entrySet().stream().filter(component -> {
|
||||
if (component.getValue() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Matcher m = p.matcher(component.getValue().toString());
|
||||
|
||||
if (m.matches()) {
|
||||
m.reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
m.reset();
|
||||
return true;
|
||||
}).collect(Collectors.toMap(Entry::getKey, Entry::getValue));
|
||||
}
|
||||
|
||||
Map<String, Object> applyAliases(Map<String, Object> components) {
|
||||
Map<String, Object> aliasedComponents = new HashMap<>();
|
||||
components.forEach((key, value) -> {
|
||||
String newKey = key;
|
||||
Iterator<JsonNode> iterator = Templates.ALIASES.getData().elements();
|
||||
while (iterator.hasNext()) {
|
||||
JsonNode pair = iterator.next();
|
||||
if (pair.get("alias").asText().equals(key)
|
||||
&& components.get(pair.get("name").asText()) == null) {
|
||||
newKey = pair.get("name").asText();
|
||||
break;
|
||||
}
|
||||
}
|
||||
aliasedComponents.put(key, value);
|
||||
aliasedComponents.put(newKey, value);
|
||||
});
|
||||
|
||||
return aliasedComponents;
|
||||
}
|
||||
|
||||
JsonNode findTemplate(Map<String, Object> components) {
|
||||
JsonNode template;
|
||||
if (Templates.WORLDWIDE.getData().has(components.get("country_code").toString())) {
|
||||
template = Templates.WORLDWIDE.getData().get(components.get("country_code").toString());
|
||||
} else {
|
||||
template = Templates.WORLDWIDE.getData().get("default");
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
JsonNode chooseTemplateText(JsonNode template, Map<String, Object> components) {
|
||||
JsonNode selected;
|
||||
if (template.has("address_template")) {
|
||||
if (Templates.WORLDWIDE.getData().has(template.get("address_template").asText())) {
|
||||
selected = Templates.WORLDWIDE.getData().get(template.get("address_template").asText());
|
||||
} else {
|
||||
selected = template.get("address_template");
|
||||
}
|
||||
} else {
|
||||
JsonNode defaults = Templates.WORLDWIDE.getData().get("default");
|
||||
selected = Templates.WORLDWIDE.getData().get(defaults.get("address_template").textValue());
|
||||
}
|
||||
|
||||
List<String> required = Arrays.asList("road", "postcode");
|
||||
long count = required.stream().filter(req -> !components.containsKey(req)).count();
|
||||
if (count == 2) {
|
||||
if (template.has("fallback_template")) {
|
||||
if (Templates.WORLDWIDE.getData().has(template.get("fallback_template").asText())) {
|
||||
selected = Templates.WORLDWIDE.getData().get(template.get("fallback_template").asText());
|
||||
} else {
|
||||
selected = template.get("fallback_template");
|
||||
}
|
||||
} else {
|
||||
JsonNode defaults = Templates.WORLDWIDE.getData().get("default");
|
||||
selected = Templates.WORLDWIDE.getData().get(defaults.get("fallback_template").textValue());
|
||||
}
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
String getStateCode(String state, String countryCode) {
|
||||
if (!Templates.STATE_CODES.getData().has(countryCode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
JsonNode countryCodes = Templates.STATE_CODES.getData().get(countryCode);
|
||||
Iterator<String> iterator = countryCodes.fieldNames();
|
||||
return StreamSupport
|
||||
.stream(Spliterators.spliteratorUnknownSize(iterator,
|
||||
Spliterator.ORDERED), false).filter(key-> {
|
||||
JsonNode code = countryCodes.get(key);
|
||||
if (code.isObject()) {
|
||||
if (code.has("default")) {
|
||||
return code.get("default").asText().toUpperCase().equals(state.toUpperCase());
|
||||
}
|
||||
} else {
|
||||
return code.asText().toUpperCase().equals(state.toUpperCase());
|
||||
}
|
||||
return false;
|
||||
}).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
String getCountyCode(String county, String countryCode) {
|
||||
if (!Templates.COUNTY_CODES.getData().has(countryCode)) {
|
||||
return null;
|
||||
}
|
||||
JsonNode country = Templates.COUNTY_CODES.getData().get(countryCode);
|
||||
Optional<JsonNode> countyCode = StreamSupport.stream(country.spliterator(), true).filter(posCounty -> {
|
||||
if (posCounty.isObject()) {
|
||||
if (posCounty.has("default")) {
|
||||
return posCounty.get("default").asText().toUpperCase().equals(county.toUpperCase());
|
||||
}
|
||||
} else {
|
||||
return posCounty.asText().toUpperCase().equals(county.toUpperCase());
|
||||
}
|
||||
return false;
|
||||
}).findFirst();
|
||||
|
||||
return countyCode.map(JsonNode::asText).orElse(null);
|
||||
}
|
||||
|
||||
String renderTemplate(JsonNode template, Map<String, Object> components) {
|
||||
Map<String, Object> callback = new HashMap<>();
|
||||
callback.put("first", (Function<String, String>) s -> {
|
||||
String[] splitted = s.split("\\s*\\|\\|\\s*");
|
||||
Optional<String> chosen = Arrays.stream(splitted).filter(v -> v.length() > 0).findFirst();
|
||||
return chosen.orElse("");
|
||||
});
|
||||
|
||||
JsonNode templateText = chooseTemplateText(template, components);
|
||||
MustacheFactory mf = new DefaultMustacheFactory();
|
||||
Mustache m = mf.compile(new StringReader(templateText.asText()), "example");
|
||||
StringWriter st = new StringWriter();
|
||||
m.execute(st, new Object[]{ components, callback});
|
||||
String rendered = cleanupRender(st.toString());
|
||||
|
||||
if (template.has("postformat_replace")) {
|
||||
ArrayNode postformat = (ArrayNode) template.get("postformat_replace");
|
||||
for (JsonNode regex : postformat) {
|
||||
Pattern p = regexPatternCache.get(regex.get(0).asText());
|
||||
Matcher m2 = p.matcher(rendered);
|
||||
rendered = m2.replaceAll(regex.get(1).asText());
|
||||
}
|
||||
}
|
||||
rendered = cleanupRender(rendered);
|
||||
String trimmed = rendered.trim();
|
||||
|
||||
return trimmed + "\n";
|
||||
}
|
||||
|
||||
String cleanupRender(String rendered) {
|
||||
Set<Map.Entry<String, String>> entries = replacements.entrySet();
|
||||
String deduped = rendered;
|
||||
|
||||
for(Map.Entry<String, String> replacement : entries) {
|
||||
Pattern p = regexPatternCache.get(replacement.getKey());
|
||||
Matcher m = p.matcher(deduped);
|
||||
String predupe = m.replaceAll(replacement.getValue());
|
||||
deduped = dedupe(predupe);
|
||||
}
|
||||
|
||||
return deduped;
|
||||
}
|
||||
|
||||
String dedupe(String rendered) {
|
||||
return Arrays.stream(rendered.split("\n"))
|
||||
.map(s -> Arrays.stream(s.trim().split(", "))
|
||||
.map(String::trim).distinct().collect(Collectors.joining(", ")))
|
||||
.distinct()
|
||||
.collect(Collectors.joining("\n"));
|
||||
}
|
||||
|
||||
static List<String> getKnownComponents() {
|
||||
List<String> knownComponents = new ArrayList<>();
|
||||
Iterator<JsonNode> fields = Templates.ALIASES.getData().elements();
|
||||
while (fields.hasNext()) {
|
||||
JsonNode field = fields.next();
|
||||
knownComponents.add(field.get("alias").textValue());
|
||||
}
|
||||
|
||||
return knownComponents;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.woheller69.AndroidAddressFormatter;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
class RegexPatternCache {
|
||||
private Map<String, Pattern> map = new HashMap<>();
|
||||
|
||||
public Pattern get(String key) {
|
||||
if (map.containsKey(key)) {
|
||||
return map.get(key);
|
||||
} else {
|
||||
Pattern newPattern = Pattern.compile(key);
|
||||
map.put(key, newPattern);
|
||||
return newPattern;
|
||||
}
|
||||
}
|
||||
|
||||
public Pattern get(String key, int flags) {
|
||||
if (map.containsKey(key)) {
|
||||
return map.get(key);
|
||||
} else {
|
||||
Pattern newPattern = Pattern.compile(key, flags);
|
||||
map.put(key, newPattern);
|
||||
return newPattern;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.woheller69.AndroidAddressFormatter;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
enum Templates {
|
||||
WORLDWIDE("worldwide.json"),
|
||||
COUNTRY_NAMES("countrynames.json"),
|
||||
ALIASES("aliases.json"),
|
||||
ABBREVIATIONS("abbreviations.json"),
|
||||
COUNTRY_2_LANG("country2lang.json"),
|
||||
COUNTY_CODES("countycodes.json"),
|
||||
STATE_CODES("statecodes.json")
|
||||
;
|
||||
|
||||
private interface Constants {
|
||||
ObjectMapper jsonWriter = new ObjectMapper();
|
||||
}
|
||||
|
||||
private final JsonNode data;
|
||||
|
||||
Templates(String fileName) {
|
||||
this.data = setData(fileName);
|
||||
}
|
||||
|
||||
synchronized public JsonNode getData() {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
synchronized private static JsonNode setData(String fileName) {
|
||||
JsonNode node = null;
|
||||
try {
|
||||
ClassLoader cl = Thread.currentThread().getContextClassLoader();
|
||||
|
||||
InputStream is = cl.getResourceAsStream(fileName);
|
||||
node = Constants.jsonWriter.readTree(is);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
1
libs/address-formatter/src/main/resources/aliases.json
Normal file
1
libs/address-formatter/src/main/resources/aliases.json
Normal file
@@ -0,0 +1 @@
|
||||
[{"alias":"street_number","name":"house_number"},{"alias":"housenumber","name":"house_number"},{"alias":"house_number","name":"house_number"},{"alias":"building","name":"house"},{"alias":"public_building","name":"house"},{"alias":"isolated_dwelling","name":"house"},{"alias":"farmland","name":"house"},{"alias":"allotments","name":"house"},{"alias":"house","name":"house"},{"alias":"footway","name":"road"},{"alias":"street","name":"road"},{"alias":"street_name","name":"road"},{"alias":"residential","name":"road"},{"alias":"path","name":"road"},{"alias":"pedestrian","name":"road"},{"alias":"road_reference","name":"road"},{"alias":"road_reference_intl","name":"road"},{"alias":"square","name":"road"},{"alias":"place","name":"road"},{"alias":"road","name":"road"},{"alias":"locality","name":"hamlet"},{"alias":"croft","name":"hamlet"},{"alias":"hamlet","name":"hamlet"},{"alias":"village","name":"village"},{"alias":"suburb","name":"neighbourhood"},{"alias":"city_district","name":"neighbourhood"},{"alias":"district","name":"neighbourhood"},{"alias":"quarter","name":"neighbourhood"},{"alias":"borough","name":"neighbourhood"},{"alias":"city_block","name":"neighbourhood"},{"alias":"residential","name":"neighbourhood"},{"alias":"commercial","name":"neighbourhood"},{"alias":"industrial","name":"neighbourhood"},{"alias":"houses","name":"neighbourhood"},{"alias":"subdistrict","name":"neighbourhood"},{"alias":"subdivision","name":"neighbourhood"},{"alias":"ward","name":"neighbourhood"},{"alias":"neighbourhood","name":"neighbourhood"},{"alias":"postal_city","name":"postal_city"},{"alias":"town","name":"city"},{"alias":"township","name":"city"},{"alias":"city","name":"city"},{"alias":"local_administrative_area","name":"municipality"},{"alias":"subcounty","name":"municipality"},{"alias":"municipality","name":"municipality"},{"alias":"county_code","name":"county"},{"alias":"department","name":"county"},{"alias":"county","name":"county"},{"alias":"state_district","name":"state_district"},{"alias":"postal_code","name":"postcode"},{"alias":"partial_postcode","name":"postcode"},{"alias":"postcode","name":"postcode"},{"alias":"province","name":"state"},{"alias":"state_code","name":"state"},{"alias":"state","name":"state"},{"alias":"region","name":"region"},{"alias":"island","name":"island"},{"alias":"archipelago","name":"archipelago"},{"alias":"country_name","name":"country"},{"alias":"country","name":"country"},{"alias":"country_code","name":"country_code"},{"alias":"continent","name":"continent"}]
|
||||
@@ -0,0 +1 @@
|
||||
{"AD":["CA"],"AE":["AR","EN"],"AF":["FA","PS"],"AG":["EN"],"AI":["EN"],"AL":["SQ"],"AM":["HY"],"AO":["PT"],"AQ":["EN"],"AR":["ES"],"AS":["EN"],"AT":["DE"],"AU":["EN"],"AW":["NL"],"AX":["FI","SE"],"AZ":["AZ"],"BA":["BS","HR","SR"],"BB":["EN"],"BD":["BN"],"BE":["FR","NL","DE"],"BF":["FR"],"BG":["BG"],"BH":["AR","EN"],"BI":["EN"],"BJ":["FR"],"BL":["FR"],"BM":["EN"],"BN":["EN","MS"],"BO":["ES"],"BQ":["NL","EN"],"BR":["PT"],"BS":["EN"],"BT":["DZ"],"BV":["NO"],"BW":["EN"],"BY":["BY","RU"],"BZ":["EN","ES"],"CA":["EN","FR"],"CC":["EN"],"CD":["FR"],"CF":["FR"],"CG":["FR"],"CH":["DE","FR","IT"],"CI":["FR"],"CK":["EN"],"CL":["ES"],"CM":["EN"],"CN":["ZH"],"CO":["ES"],"CR":["ES"],"CU":["ES"],"CV":["PT"],"CW":["EN","NL"],"CX":["EN"],"CY":["HE"],"CZ":["CS"],"DE":["DE"],"DJ":["AR","FR"],"DK":["DA"],"DM":["EN"],"DO":["ES"],"DZ":["AR","FR"],"EC":["ES"],"EE":["ET"],"EG":["AR"],"EH":["AR"],"ER":["AR","IT","TI"],"ES":["CA","ES","EU","GL"],"ET":["AM","OM","SO","TI"],"FI":["FI","SE"],"FJ":["EN"],"FK":["EN"],"FM":["EN"],"FO":["DA"],"FR":["FR"],"GA":["FR"],"GB":["EN"],"GD":["EN"],"GE":["KA"],"GG":["EN"],"GF":["FR"],"GH":["EN"],"GI":["EN"],"GL":["DA","KL"],"GM":["EN"],"GN":["FR"],"GP":["FR"],"GQ":["ES","FR","PT"],"GR":["EL"],"GS":["EN"],"GT":["ES"],"GU":["EN"],"GW":["PT"],"GY":["EN"],"HK":["EN","ZH"],"HM":["EN"],"HN":["ES"],"HR":["HR"],"HT":["FR","HT"],"HU":["HU"],"ID":["ID"],"IE":["EN"],"IL":["AR","HE"],"IM":["EN"],"IN":["EN"],"IO":["EN"],"IQ":["AR"],"IR":["FA"],"IS":["IS"],"IT":["IT"],"JE":["EN"],"JM":["EN"],"JO":["AR"],"JP":["JP"],"KE":["EN"],"KG":["KY","RU"],"KH":["KM"],"KI":["EN"],"KM":["AR","FR"],"KN":["EN"],"KP":["KO"],"KR":["KO"],"KW":["AR"],"KY":["EN"],"KZ":["KK","RU"],"LA":["LO"],"LB":["AR"],"LC":["EN"],"LI":["DE"],"LK":["EN","SI","TA"],"LR":["EN"],"LS":["EN","ST"],"LT":["LT"],"LU":["DE","FR"],"LV":["LV"],"LY":["AR"],"MA":["AR","FR"],"MC":["FR"],"MD":["RO"],"ME":["SR"],"MF":["FR"],"MG":["FR","MG"],"MH":["EN"],"MK":["MK"],"ML":["FR"],"MM":["MY"],"MN":["MN"],"MO":["PT","ZH"],"MP":["EN"],"MQ":["FR"],"MR":["AR","FR"],"MS":["EN"],"MT":["EN","MT"],"MU":["EN","FR"],"MV":["DV"],"MW":["EN","NY"],"MX":["ES"],"MY":["MS"],"MZ":["PT"],"NA":["EN"],"NC":["FR"],"NE":["FR"],"NF":["EN"],"NG":["ES"],"NI":["ES"],"NL":["NL"],"NO":["NO"],"NP":["EN"],"NR":["EN"],"NU":["EN"],"NZ":["EN"],"OM":["AR","EN"],"PA":["ES"],"PE":["ES"],"PF":["FR"],"PG":["EN"],"PH":["EN","TL"],"PK":["EN"],"PL":["PL"],"PM":["FR"],"PN":["EN"],"PR":["EN","ES"],"PS":["AR"],"PT":["PT"],"PW":["EN"],"PY":["ES"],"QA":["AR","EN"],"RE":["FR"],"RO":["RO"],"RS":["SR"],"RU":["RU"],"RW":["EN","FR","RW"],"SA":["AR"],"SB":["EN"],"SC":["EN","FR"],"SD":["AR","EN"],"SE":["SE"],"SG":["EN"],"SH":["EN"],"SI":["SL"],"SJ":["NO"],"SK":["SK"],"SL":["EN"],"SM":["IT"],"SN":["FR"],"SO":["SO"],"SR":["NL"],"SS":["EN"],"ST":["PT"],"SV":["ES"],"SX":["EN","NL"],"SY":["AR"],"SZ":["EN","SS"],"TC":["EN"],"TD":["FR"],"TF":["FR"],"TG":["FR"],"TH":["TH"],"TJ":["RU","TG"],"TK":["EN"],"TL":["EN","ID","PT"],"TN":["AR","FR"],"TM":["RU","TK"],"TO":["EN","TO"],"TR":["TR"],"TT":["EN"],"TV":["EN"],"TW":["ZH"],"TZ":["EN","SW"],"UA":["UK"],"UG":["EN","SW"],"UM":["EN"],"US":["EN"],"UY":["ES"],"UZ":["UZ"],"VA":["IT"],"VC":["EN"],"VE":["ES"],"VG":["EN"],"VI":["EN"],"VN":["VN"],"VU":["EN"],"WF":["FR"],"WS":["EN"],"YE":["AR"],"YT":["FR"],"ZA":["EN"],"ZM":["EN"],"ZW":["EN"]}
|
||||
@@ -0,0 +1 @@
|
||||
{"AD":"Andorra","AE":"United Arab Emirates","AF":"Afghanistan","AG":"Antigua and Barbuda","AI":"Anguilla","AL":"Albania","AM":"Armenia","AO":"Angola","AQ":"Antarctica","AR":"Argentina","AS":"American Samoa","AT":"Austria","AU":"Australia","AW":"Aruba","AX":"Åland Islands","AZ":"Azerbaijan","BA":"Bosnia and Herzegovina","BB":"Barbados","BD":"Bangladesh","BE":"Belgium","BF":"Burkina Faso","BG":"Bulgaria","BH":"Bahrain","BI":"Burundi","BJ":"Benin","BL":"Saint Barthélemy","BM":"Bermuda","BN":"Brunei","BO":"Bolivia","BQ":"Caribbean Netherlands","BR":"Brazil","BS":"The Bahamas","BT":"Bhutan","BV":"Bouvet Island","BW":"Botswana","BY":"Belarus","BZ":"Belize","CA":"Canada","CC":"Cocos (Keeling) Islands","CD":"Democratic Republic of the Congo","CF":"Central African Republic","CG":"Republic of the Congo","CH":"Switzerland","CI":"Côte d'Ivoire","CK":"Cook Islands","CL":"Chile","CM":"Cameroon","CN":"China","CO":"Colombia","CR":"Costa Rica","CU":"Cuba","CV":"Cabo Verde","CW":"Curaçao","CX":"Christmas Island","CY":"Cyprus","CZ":"Czech Republic","DE":"Germany","DJ":"Djibouti","DK":"Denmark","DM":"Dominica","DO":"Dominican Republic","DZ":"Algeria","EC":"Ecuador","EE":"Estonia","EG":"Egypt","EH":"Western Sahara","ER":"Eritrea","ES":"Spain","ET":"Ethiopia","FI":"Finland","FJ":"Fiji","FK":"Falkland Islands","FM":"Federated States of Micronesia","FO":"Faroe Islands","FR":"France","GA":"Gabon","GB":"United Kingdom","GD":"Grenada","GE":"Georgia (country)","GF":"French Guiana","GG":"Guernsey","GH":"Ghana","GI":"Gibraltar","GL":"Greenland","GM":"The Gambia","GN":"Guinea","GP":"Guadeloupe","GQ":"Equatorial Guinea","GR":"Greece","GS":"South Georgia and the South Sandwich Islands","GT":"Guatemala","GU":"Guam","GW":"Guinea-Bissau","GY":"Guyana","HK":"Hong Kong","HM":"Heard Island and McDonald Islands","HN":"Honduras","HR":"Croatia","HT":"Haiti","HU":"Hungary","ID":"Indonesia","IE":"Republic of Ireland","IL":"Israel","IM":"Isle of Man","IN":"India","IO":"British Indian Ocean Territory","IQ":"Iraq","IR":"Iran","IS":"Iceland","IT":"Italy","JE":"Jersey","JM":"Jamaica","JO":"Jordan","JP":"Japan","KE":"Kenya","KG":"Kyrgyzstan","KH":"Cambodia","KI":"Kiribati","KM":"Comoros","KN":"Saint Kitts and Nevis","KP":"North Korea","KR":"South Korea","KW":"Kuwait","KY":"Cayman Islands","KZ":"Kazakhstan","LA":"Laos","LB":"Lebanon","LC":"Saint Lucia","LI":"Liechtenstein","LK":"Sri Lanka","LR":"Liberia","LS":"Lesotho","LT":"Lithuania","LU":"Luxembourg","LV":"Latvia","LY":"Libya","MA":"Morocco","MC":"Monaco","MD":"Moldova","ME":"Montenegro","MF":"Collectivity of Saint Martin","MG":"Madagascar","MH":"Marshall Islands","MK":"Republic of North Macedonia","ML":"Mali","MM":"Myanmar","MN":"Mongolia","MO":"Macau","MP":"Northern Mariana Islands","MQ":"Martinique","MR":"Mauritania","MS":"Montserrat","MT":"Malta","MU":"Mauritius","MV":"Maldives","MW":"Malawi","MX":"Mexico","MY":"Malaysia","MZ":"Mozambique","NA":"Namibia","NC":"New Caledonia","NE":"Niger","NF":"Norfolk Island","NG":"Nigeria","NI":"Nicaragua","NL":"Netherlands","NO":"Norway","NP":"Nepal","NR":"Nauru","NU":"Niue","NZ":"New Zealand","OM":"Oman","PA":"Panama","PE":"Peru","PF":"French Polynesia","PG":"Papua New Guinea","PH":"Philippines","PK":"Pakistan","PL":"Poland","PM":"Saint Pierre and Miquelon","PN":"Pitcairn Islands","PR":"Puerto Rico","PS":"State of Palestine","PT":"Portugal","PW":"Palau","PY":"Paraguay","QA":"Qatar","RE":"Réunion","RO":"Romania","RS":"Serbia","RU":"Russia","RW":"Rwanda","SA":"Saudi Arabia","SB":"Solomon Islands","SC":"Seychelles","SD":"Sudan","SE":"Sweden","SG":"Singapore","SH":"Saint Helena, Ascension and Tristan da Cunha","SI":"Slovenia","SJ":"Svalbard and Jan Mayen","SK":"Slovakia","SL":"Sierra Leone","SM":"San Marino","SN":"Senegal","SO":"Somalia","SR":"Suriname","SS":"South Sudan","ST":"São Tomé and Príncipe","SV":"El Salvador","SX":"Sint Maarten","SY":"Syria","SZ":"Swaziland","TC":"Turks and Caicos Islands","TD":"Chad","TF":"French Southern and Antarctic Lands","TG":"Togo","TH":"Thailand","TJ":"Tajikistan","TK":"Tokelau","TL":"East Timor","TM":"Turkmenistan","TN":"Tunisia","TO":"Tonga","TR":"Turkey","TT":"Trinidad and Tobago","TV":"Tuvalu","TW":"Taiwan","TZ":"Tanzania","UA":"Ukraine","UG":"Uganda","UM":"United States Minor Outlying Islands","US":"United States","UY":"Uruguay","UZ":"Uzbekistan","VA":"Vatican City","VC":"Saint Vincent and the Grenadines","VE":"Venezuela","VG":"British Virgin Islands","VI":"United States Virgin Islands","VN":"Vietnam","VU":"Vanuatu","WF":"Wallis and Futuna","WS":"Samoa","YE":"Yemen","YT":"Mayotte","ZA":"South Africa","ZM":"Zambia","ZW":"Zimbabwe"}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
libs/address-formatter/src/main/resources/worldwide.json
Normal file
1
libs/address-formatter/src/main/resources/worldwide.json
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user