Java tutorial
/* * Copyright 2016 Danish Maritime Authority. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.niord.core.aton; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import javax.annotation.PostConstruct; import javax.annotation.Resource; import javax.ejb.*; import javax.inject.Inject; import javax.xml.bind.JAXBContext; import javax.xml.bind.Unmarshaller; import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; import javax.xml.transform.Result; import javax.xml.transform.Source; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; import java.io.StringReader; import java.io.StringWriter; import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; /** * Creates default OSM node-tag sets based on INT-1-Presets.xml based on: * https://raw.githubusercontent.com/OpenSeaMap/josm/master/INT-1-preset.xml * * <p> * NB: Remove the "xmlns" attribute from the root <presets> element before checking the file into this project. * <p> * The XML file is transformed to a more manageable format for relevant node types, via the aton-osm-defaults.xslt * transformation. Snippet from result XML file: * * <pre> * <osm-defaults> * <tag-values id="buoyshapes"> * <tag-value v="conical"/> * <tag-value v="can"/> * <tag-value v="spherical"/> * <tag-value v="super-buoy"/> * <tag-value v="pillar"/> * <tag-value v="spar"/> * <tag-value v="barrel"/> * <tag-value v="ice-buoy"/> * </tag-values> * ... * <node-type name="Isolated Danger Topmark"> * <tag k="seamark:topmark:shape" v="2 spheres"/> * <tag k="seamark:topmark:colour" v="black"/> * <tag k="seamark:topmark:colour_patern" v=""/> * </node-type> * <node-type name="Isolated Danger Buoy"> * <tag k="seamark:type" v="buoy_isolated_danger"/> * <tag k="seamark:buoy_isolated_danger:shape"> * <tag-values ref="buoyshapes"/> * </tag> * <tag k="seamark:buoy_isolated_danger:colour"> * <tag-values ref="cardinalcolours"/> * </tag> * <tag k="seamark:buoy_isolated_danger:colour_pattern"> * <tag-values ref="patterns"/> * </tag> * <tag k="seamark:name"/> * </node-type> * ... * </pre> * * <p> * Testing: From the command lines, run: * <pre> * xsltproc --stringparam ialaSkipSystem "IALA-B" aton-osm-defaults.xslt INT-1-preset.xml > result.xml * </pre> * */ @Singleton @Startup @SuppressWarnings("unused") public class AtonDefaultsService { @Inject private Logger log; @Resource TimerService timerService; // TODO: Inject from setting private IalaBuoyageSystem ialaSystem = IalaBuoyageSystem.IALA_A; private OsmDefaults osmDefaults; // Look-up table for tag-values private Map<String, ODTagValues> osmTagValues = new HashMap<>(); private Map<String, ODNodeType> osmNodeTypes = new HashMap<>(); /** Called upon application startup */ @PostConstruct public void init() { // In order not to stall webapp deployment, wait 3 seconds before initializing the defaults timerService.createSingleActionTimer(3000, new TimerConfig()); } /** * Generates the AtoN defaults from the INT-1-preset.xml file */ @Timeout private void generateDefaults() { try { long t0 = System.currentTimeMillis(); Source xsltSource = new StreamSource(getClass().getResourceAsStream("/aton/aton-osm-defaults.xslt")); Source xmlSource = new StreamSource(getClass().getResourceAsStream("/aton/INT-1-preset.xml")); // Capture the generated xml as a string StringWriter xml = new StringWriter(); Result result = new StreamResult(xml); // Execute the xslt TransformerFactory transFact = TransformerFactory.newInstance(); Transformer trans = transFact.newTransformer(xsltSource); trans.setParameter("ialaSkipSystem", ialaSystem.other().toString()); trans.transform(xmlSource, result); // Fix spelling mistakes String resultXml = xml.toString(); resultXml = resultXml.replace("topamrk", "topmark").replace("patern", "pattern"); // Read in the result as OsmDefaults data JAXBContext jaxbContext = JAXBContext.newInstance(OsmDefaults.class); Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); osmDefaults = (OsmDefaults) unmarshaller.unmarshal(new StringReader(resultXml)); // Build look-up tables for fast access osmDefaults.getTagValues().stream().forEach(tv -> osmTagValues.put(tv.getId(), tv)); osmDefaults.getNodeTypes().stream().forEach(nt -> osmNodeTypes.put(nt.getName(), nt)); log.trace("Created AtoN defaults in " + (System.currentTimeMillis() - t0) + " ms"); } catch (Exception e) { log.error("Failed creating AtoN defaults in " + e, e); } } /** * Returns the name of all node types where the name matches the parameter * * @param name the substring match * @return the name of all node types where the name matches the parameter */ public List<String> getNodeTypeNames(String name) { return osmDefaults.getNodeTypes().stream().map(ODNodeType::getName) .filter(n -> name == null || StringUtils.containsIgnoreCase(n, name)).distinct().sorted() .collect(Collectors.toList()); } /** * Merges the given AtoN with the tags of the node type with the given name * * @param aton the AtoN to update * @param nodeTypeName type names */ public void mergeAtonWithNodeTypes(AtonNode aton, String nodeTypeName) { // Sanity checks if (aton == null || StringUtils.isBlank(nodeTypeName) || !osmNodeTypes.containsKey(nodeTypeName)) { return; } osmNodeTypes.get(nodeTypeName).getTags().forEach(tag -> { if (aton.getTagValue(tag.getK()) == null) { String v = StringUtils.defaultString(tag.getV()); aton.getTags().add(new AtonTag(tag.getK(), v)); } }); } /** * Computes an auto-complete list for OSM tag keys, based on the current AtoN and key * * @param aton the current AtoN * @param keyStr the currently typed key * @param maxKeyNo the max number of keys to return * @return the auto-complete list */ public List<String> computeKeysForAton(AtonNode aton, String keyStr, int maxKeyNo) { // Return empty result for empty key string if (StringUtils.isBlank(keyStr)) { return Collections.emptyList(); } List<ODNodeType> matchingNodeTypes = computeMatchingNodeTypes(aton); Set<String> existingTagKeys = aton.getTags().stream().map(AtonTag::getK).collect(Collectors.toSet()); // Filter the tag keys of the matching node types, such that // 1) There is a substring match with the "key" param // 2) The key is not already defined in the AtoN List<String> result = matchingNodeTypes.stream().flatMap(nt -> nt.getTags().stream()).map(ODTag::getK) .filter(k -> StringUtils.containsIgnoreCase(k, keyStr)).filter(k -> !existingTagKeys.contains(k)) .distinct().limit(maxKeyNo).collect(Collectors.toList()); // If there is no match from the matching node types, just look for any matching tag key if (result.isEmpty()) { result = osmDefaults.getNodeTypes().stream().flatMap(nt -> nt.getTags().stream()).map(ODTag::getK) .filter(k -> StringUtils.containsIgnoreCase(k, keyStr)).distinct().limit(maxKeyNo).sorted() .collect(Collectors.toList()); } return result; } /** * Creates an auto-complete list for OSM tag values, based on the current AtoN, key and value * * @param aton the current AtoN * @param key the current key * @param valueStr the currently typed value * @param maxValueNo the max number of values to return * @return the auto-complete list */ public List<String> getValuesForAtonAndKey(AtonNode aton, String key, String valueStr, int maxValueNo) { // Return empty result for empty key if (StringUtils.isBlank(key)) { return Collections.emptyList(); } // Find a tag with a matching key in the set of node types that matches the AtoN List<ODNodeType> matchingNodeTypes = computeMatchingNodeTypes(aton); List<String> values = matchingNodeTypes.stream().map(nt -> computeValuesForNodeType(nt, key, valueStr)) .filter(v -> !v.isEmpty()).flatMap(Collection::stream).distinct().limit(maxValueNo) .collect(Collectors.toList()); // If we did not find any matching key-value in the set of node types // that matches the AtoN, look for any matching key-value if (values.isEmpty()) { values = osmDefaults.getNodeTypes().stream().map(nt -> computeValuesForNodeType(nt, key, valueStr)) .filter(v -> !v.isEmpty()).flatMap(Collection::stream).distinct().limit(maxValueNo) .collect(Collectors.toList()); } return values; } /** * Computes the tag values for the given node type, key and value substring. * @param nodeType the node type * @param key the tag key * @param valueStr the value string * @return the tag values for the given node type, key and value substring */ private List<String> computeValuesForNodeType(ODNodeType nodeType, String key, String valueStr) { ODTag tag = nodeType.tag(key); return computeValuesForTag(tag).stream() .filter(v -> valueStr == null || StringUtils.containsIgnoreCase(v, valueStr)).distinct() .collect(Collectors.toList()); } /** * Compute the list of values for the given tag. The tag can have various formats: * * <p> * Format 1) Tag defines the value in "v" attribute * <pre> * <tag k="seamark:notice:function" v="information"/> * </pre> * <p> * Format 2) Tag defines values in "tag-value" sub-elements * <pre> * <tag k="seamark:buoy_lateral:category"> * <tag-value v="danger_right"/> * <tag-value v="junction_right"/> * <tag-value v="turnoff_right"/> * <tag-value v="harbour_right"/> * </tag> * </pre> * Format 3) defines values in "tag-values" value list references * <pre> * <tag k="seamark:buoy_lateral:colour"> * <tag-values ref="rightlateralcolours"/> * <tag-values ref="leftlateralcolours"/> * </tag> * </pre> * <p> * * @param tag the tag to find the values for * @return the list of values */ private List<String> computeValuesForTag(ODTag tag) { if (tag == null) { return Collections.emptyList(); } else if (StringUtils.isNotBlank(tag.getV())) { // The tag explicitly defines the value in the "v" attribute return Collections.singletonList(tag.getV()); } // Look for "<tag-values ref="id"/>" sub-elements if (tag.getTagValueRefs() != null) { return tag.getTagValueRefs().stream() .filter(tvs -> tvs.getRef() != null && osmTagValues.containsKey(tvs.getRef())) .map(tvs -> osmTagValues.get(tvs.getRef())).filter(tvs -> tvs != null && tvs.getTags() != null) .flatMap(tvs -> tvs.getTags().stream()).map(ODTagValue::getV).collect(Collectors.toList()); } else if (tag.getTagValues() != null) { // Look for "<tag-value v="value"/>" sub-elements return tag.getTagValues().stream().map(ODTagValue::getV).collect(Collectors.toList()); } // No joy return Collections.emptyList(); } /** * Returns a list of matching node types for the given AtoN, sorted * so that the first node types have a higher match with the AtoN. * * @param aton the AtoN * @return a list of matching node types for the given AtoN */ private List<ODNodeType> computeMatchingNodeTypes(AtonNode aton) { // Compute an AtoN match score for each node type Map<ODNodeType, Integer> nodeTypeScore = osmDefaults.getNodeTypes().stream() .collect(Collectors.toMap(Function.identity(), nt -> computeNodeTypeMatch(aton, nt))); // Returns all node types with a non-trivial match (score > 2) sorted by the score return nodeTypeScore.keySet().stream().filter(nt -> nodeTypeScore.get(nt) > 2) .sorted((nt1, nt2) -> nodeTypeScore.get(nt2).compareTo(nodeTypeScore.get(nt1))) .collect(Collectors.toList()); } /** * Returns a score for match between the AtoN and the node type * * @param aton the AtoN * @param nodeType the node type * @return a score for match between the AtoN and the node type */ private int computeNodeTypeMatch(AtonNode aton, ODNodeType nodeType) { int score = 0; // Check for matching tag values score += nodeType.getTags().stream().mapToInt(t -> { AtonTag tag = aton.getTag(t.getK()); if (tag != null && Objects.equals(tag.getV(), t.getV())) { if ("seamark:type".equals(t.getK())) { return 10; // Matching "seamark:type" value } else { return 4; // Matching key and value } } else if (tag != null) { return 2; // Matching key } else { return 0; // No match } }).sum(); return score; } /*************************/ /** Helper classes **/ /*************************/ /** * Root element in the OSM Defaults data generated by the aton-osm-defaults.xslt transformation */ @XmlRootElement(name = "osm-defaults") private static class OsmDefaults { List<ODTagValues> tagValues = new ArrayList<>(); List<ODNodeType> nodeTypes; @XmlElement(name = "tag-values") List<ODTagValues> getTagValues() { return tagValues; } public void setTagValues(List<ODTagValues> tagValues) { this.tagValues = tagValues; } @XmlElement(name = "node-type") List<ODNodeType> getNodeTypes() { return nodeTypes; } public void setNodeTypes(List<ODNodeType> nodeTypes) { this.nodeTypes = nodeTypes; } } /** * Defines a node type */ private static class ODNodeType { String name; List<ODTag> tags = new ArrayList<>(); /** Returns if the node type contains a matching tag key pattern and value */ public boolean hasTagValue(String k, String v) { return tags.stream().anyMatch(t -> t.getK().matches(k) && v.equals(t.getV())); } /** Returns the tag with the given key, or null if not found */ public ODTag tag(String key) { return tags.stream().filter(t -> t.getK().equals(key)).findFirst().orElse(null); } @XmlAttribute public String getName() { return name; } public void setName(String name) { this.name = name; } @XmlElement(name = "tag") public List<ODTag> getTags() { return tags; } public void setTags(List<ODTag> tags) { this.tags = tags; } } /** * Defines a node tag */ @SuppressWarnings("unused") private static class ODTag { String k; String v; List<ODTagValues> tagValueRefs; List<ODTagValue> tagValues; @XmlAttribute public String getK() { return k; } public void setK(String k) { this.k = k; } @XmlAttribute public String getV() { return v; } public void setV(String v) { this.v = v; } @XmlElement(name = "tag-values") List<ODTagValues> getTagValueRefs() { return tagValueRefs; } public void setTagValueRefs(List<ODTagValues> tagValueRefs) { this.tagValueRefs = tagValueRefs; } @XmlElement(name = "tag-value") List<ODTagValue> getTagValues() { return tagValues; } public void setTagValues(List<ODTagValue> tagValues) { this.tagValues = tagValues; } } /** * List of tag values */ @SuppressWarnings("unused") private static class ODTagValues { String id; String ref; List<ODTagValue> tags; @XmlAttribute public String getId() { return id; } public void setId(String id) { this.id = id; } @XmlAttribute public String getRef() { return ref; } public void setRef(String ref) { this.ref = ref; } @XmlElement(name = "tag-value") public List<ODTagValue> getTags() { return tags; } public void setTags(List<ODTagValue> tags) { this.tags = tags; } } /** * Defines a tag values */ private static class ODTagValue { String v; @XmlAttribute public String getV() { return v; } public void setV(String v) { this.v = v; } } }