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.area; import org.apache.commons.lang.StringUtils; import org.niord.core.area.vo.SystemAreaVo.AreaMessageSorting; import org.niord.core.db.CriteriaHelper; import org.niord.core.domain.Domain; import org.niord.core.domain.DomainService; import org.niord.core.geojson.GeoJsonUtils; import org.niord.core.message.Message; import org.niord.core.service.BaseService; import org.niord.core.settings.Setting; import org.niord.core.settings.SettingsService; import org.niord.model.search.PagedSearchParamsVo; import org.slf4j.Logger; import javax.ejb.Schedule; import javax.ejb.Stateless; import javax.inject.Inject; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.Join; import javax.persistence.criteria.JoinType; import javax.persistence.criteria.Order; import javax.persistence.criteria.Path; import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Random; import java.util.Set; import java.util.stream.Collectors; import static org.niord.core.area.AreaSearchParams.TREE_SORT_ORDER; /** * Business interface for accessing Niord areas */ @Stateless @SuppressWarnings("unused") public class AreaService extends BaseService { public static final String SETTING_AREA_LAST_UPDATED = "areaLastUpdate"; @Inject private Logger log; @Inject SettingsService settingsService; @Inject DomainService domainService; /** * Returns the area with the given legacy id * * @param legacyId the legacy id of the area * @return the area with the given legacy id or null if not found */ public Area findByLegacyId(String legacyId) { try { return em.createNamedQuery("Area.findByLegacyId", Area.class).setParameter("legacyId", legacyId) .getSingleResult(); } catch (Exception e) { return null; } } /** * Searches for areas matching the given search params * * @param params the sesarch params * @return the search result */ @SuppressWarnings("all") public List<Area> searchAreas(AreaSearchParams params) { CriteriaBuilder cb = em.getCriteriaBuilder(); CriteriaQuery<Area> areaQuery = cb.createQuery(Area.class); Root<Area> areaRoot = areaQuery.from(Area.class); // Build the predicate CriteriaHelper<Area> criteriaHelper = new CriteriaHelper<>(cb, areaQuery); // Match the name Join<Area, AreaDesc> descs = areaRoot.join("descs", JoinType.LEFT); if (params.isExact()) { criteriaHelper.equalsIgnoreCase(descs.get("name"), params.getName()); } else { criteriaHelper.like(descs.get("name"), params.getName()); } // Optionally, match the language if (StringUtils.isNotBlank(params.getLanguage())) { criteriaHelper.equals(descs.get("lang"), params.getLanguage()); } // Optionally, match the parent if (params.getParentId() != null) { areaRoot.join("parent", JoinType.LEFT); Path<Area> parent = areaRoot.get("parent"); criteriaHelper.equals(parent.get("id"), params.getParentId()); } // Assemple lineage filters from search domain and areas Set<String> lineages = new HashSet<>(); // Optionally, filter by the areas associated with the specified domain if (StringUtils.isNotBlank(params.getDomain())) { Domain d = domainService.findByDomainId(params.getDomain()); if (d != null && d.getAreas().size() > 0) { d.getAreas().forEach(a -> lineages.add(a.getLineage())); } } // Optionally, filter by area subtrees if (params.getAreaIds() != null && !params.getAreaIds().isEmpty()) { getAreaDetails(params.getAreaIds()).forEach(a -> lineages.add(a.getLineage())); } // If defined, apply the area lineage filter if (!lineages.isEmpty()) { Predicate[] areaMatch = lineages.stream() .map(lineage -> cb.like(areaRoot.get("lineage"), lineage + "%")).toArray(Predicate[]::new); criteriaHelper.add(cb.or(areaMatch)); } // Optionally, search by type if (params.getType() != null) { criteriaHelper.add(cb.equal(areaRoot.get("type"), params.getType())); } // Optionally, require that the area has an associated geometry if (params.isGeometry()) { criteriaHelper.add(cb.isNotNull(areaRoot.get("geometry"))); } // Optionally, require that the area has an messageSorting type if (params.isMessageSorting()) { criteriaHelper.add(cb.isNotNull(areaRoot.get("messageSorting"))); } // Unless the "inactive" search flag is set, only include active areas. if (!params.isInactive()) { criteriaHelper.add(cb.equal(areaRoot.get("active"), true)); } // Compute the sort order List<Order> sortOrders = new ArrayList<>(); if (TREE_SORT_ORDER.equals(params.getSortBy())) { Arrays.asList("treeSortOrder", "siblingSortOrder", "id").forEach(field -> { if (params.getSortOrder() == PagedSearchParamsVo.SortOrder.ASC) { sortOrders.add(cb.asc(areaRoot.get(field))); } else { sortOrders.add(cb.desc(areaRoot.get(field))); } }); } // Complete the query areaQuery.select(areaRoot).distinct(true).where(criteriaHelper.where()).orderBy(sortOrders); // Execute the query and update the search result return em.createQuery(areaQuery).setMaxResults(params.getMaxSize()).getResultList(); } /** * Returns the hierarchical list of root areas. * <p> * @return the hierarchical list of root areas */ public List<Area> getAreaTree() { // Get all areas along with their AreaDesc records // Will ensure that all Area entities are cached in the entity manager before organizing the result List<Area> areas = em.createNamedQuery("Area.findAreasWithDescs", Area.class).getResultList(); // Extract the roots return areas.stream().filter(Area::isRootArea).sorted().collect(Collectors.toList()); } /** * Looks up an area * * @param id the id of the area * @return the area */ public Area getAreaDetails(Integer id) { return getByPrimaryKey(Area.class, id); } /** * Looks up the areas with the given IDs * * @param ids the ids of the area * @return the area */ public List<Area> getAreaDetails(Set<Integer> ids) { return em.createNamedQuery("Area.findAreasWithIds", Area.class).setParameter("ids", ids).getResultList(); } /** * Updates the area data from the area template, but not the parent-child hierarchy of the area * * @param area the area to update * @return the updated area */ public Area updateAreaData(Area area) { Area original = getByPrimaryKey(Area.class, area.getId()); original.setMrn(area.getMrn()); original.setType(area.getType()); original.setActive(area.isActive()); original.setSiblingSortOrder(area.getSiblingSortOrder()); original.copyDescsAndRemoveBlanks(area.getDescs()); original.setGeometry(area.getGeometry()); original.setMessageSorting(area.getMessageSorting()); original.setOriginLatitude(area.getOriginLatitude()); original.setOriginLongitude(area.getOriginLongitude()); original.setOriginAngle(area.getOriginAngle()); original.getEditorFields().clear(); original.getEditorFields().addAll(area.getEditorFields()); original.updateLineage(); original.updateActiveFlag(); original = saveEntity(original); return original; } /** * Creates a new area based on the area template * @param area the area to create * @param parentId the id of the parent area * @return the created area */ public Area createArea(Area area, Integer parentId) { if (parentId != null) { Area parent = getByPrimaryKey(Area.class, parentId); parent.addChild(area); } area = saveEntity(area); // The area now has an ID - Update lineage area.updateLineage(); area.updateActiveFlag(); area = saveEntity(area); em.flush(); return area; } /** * Moves the area to the given parent id * @param areaId the id of the area to create * @param parentId the id of the parent area * @return if the area was moved */ public boolean moveArea(Integer areaId, Integer parentId) { Area area = getByPrimaryKey(Area.class, areaId); if (area.getParent() != null && area.getParent().getId().equals(parentId)) { return false; } if (area.getParent() != null) { area.getParent().getChildren().remove(area); } if (parentId == null) { area.setParent(null); } else { Area parent = getByPrimaryKey(Area.class, parentId); parent.addChild(area); } // Save the entity saveEntity(area); em.flush(); // Update all lineages updateLineages(); area.updateActiveFlag(); return true; } /** * Changes the sort order of an area, by moving it up or down compared to siblings. * <p> * Please note that by moving "up" we mean in a geographical tree structure, * i.e. a smaller sortOrder value. * * @param areaId the id of the area to move * @param moveUp whether to move the area up or down * @return if the area was moved */ public boolean changeSortOrder(Integer areaId, boolean moveUp) { Area area = getByPrimaryKey(Area.class, areaId); boolean updated = false; List<Area> siblings; if (area.getParent() != null) { siblings = area.getParent().getChildren(); } else { siblings = em.createNamedQuery("Area.findRootAreas", Area.class).getResultList(); } Collections.sort(siblings); int index = siblings.indexOf(area); // As a bootstrap issue, some sibling areas may have the same sibling sort order, e.g. 0.0. // If that is the case, simply re-assign new values if (siblings.stream().map(Area::getSiblingSortOrder).distinct().count() != siblings.size()) { for (int x = 0; x < siblings.size(); x++) { Area a = siblings.get(x); a.setSiblingSortOrder(x); saveEntity(a); } } if (moveUp) { if (index == 1) { area.setSiblingSortOrder(siblings.get(0).getSiblingSortOrder() - 10.0); updated = true; } else if (index > 1) { double so1 = siblings.get(index - 1).getSiblingSortOrder(); double so2 = siblings.get(index - 2).getSiblingSortOrder(); area.setSiblingSortOrder((so1 + so2) / 2.0); updated = true; } } else { if (index == siblings.size() - 2) { area.setSiblingSortOrder(siblings.get(siblings.size() - 1).getSiblingSortOrder() + 10.0); updated = true; } else if (index < siblings.size() - 2) { double so1 = siblings.get(index + 1).getSiblingSortOrder(); double so2 = siblings.get(index + 2).getSiblingSortOrder(); area.setSiblingSortOrder((so1 + so2) / 2.0); updated = true; } } if (updated) { log.info("Updates sort order for area " + area.getId() + " to " + area.getSiblingSortOrder()); // Save the entity saveEntity(area); } return updated; } /** * Update lineages for all areas */ public void updateLineages() { log.info("Update area lineages"); // Get root areas List<Area> roots = getAll(Area.class).stream().filter(Area::isRootArea).collect(Collectors.toList()); // Update each root subtree List<Area> updated = new ArrayList<>(); roots.forEach(area -> updateLineages(area, updated)); // Persist the changes updated.forEach(this::saveEntity); em.flush(); } /** * Recursively updates the lineages of areas rooted at the given area * @param area the area whose sub-tree should be updated * @param areas the list of updated areas * @return if the lineage was updated */ private boolean updateLineages(Area area, List<Area> areas) { boolean updated = area.updateLineage(); if (updated) { areas.add(area); } area.getChildren().forEach(childArea -> updateLineages(childArea, areas)); return updated; } /** * Deletes the area and sub-areas * @param areaId the id of the area to delete */ public boolean deleteArea(Integer areaId) { Area area = getByPrimaryKey(Area.class, areaId); if (area != null) { // Remove parent area relation area.setParent(null); saveEntity(area); remove(area); log.debug("Removed area " + areaId); return true; } return false; } /** * Looks up an area by name * @param name the name to search for * @param lang the language. Optional * @param parentId the parent ID. Optional * @return The matching area, or null if not found */ public Area findByName(String name, String lang, Integer parentId) { if (StringUtils.isBlank(name)) { return null; } AreaSearchParams params = new AreaSearchParams(); params.parentId(parentId).language(lang).inactive(true) // Also search inactive areas .name(name).exact(true) // Not substring matches .maxSize(1); List<Area> areas = searchAreas(params); return areas.isEmpty() ? null : areas.get(0); } /** * Returns the area with the given MRN. Returns null if the area is not found. * * @param mrn the MRN of the area * @return the area with the given MRN or null if not found */ public Area findByMrn(String mrn) { try { return em.createNamedQuery("Area.findByMrn", Area.class).setParameter("mrn", mrn).getSingleResult(); } catch (Exception e) { return null; } } /** * Returns the area with the given ID or MRN. Returns null if the area is not found. * * @param areaId the area ID or MRN * @return the area with the given ID or MRN or null if not found */ public Area findByAreaId(String areaId) { if (StringUtils.isNumeric(areaId)) { return getAreaDetails(Integer.valueOf(areaId)); } return findByMrn(areaId); } /** * Ensures that the template area and it's parents exists. * * @param templateArea the template area * @param create whether to create a missing area or just find it * @return the area */ public Area findOrCreateArea(Area templateArea, boolean create) { // Sanity checks if (templateArea == null) { return null; } // Check if we can find the area by MRN if (StringUtils.isNotBlank(templateArea.getMrn())) { Area area = findByMrn(templateArea.getMrn()); if (area != null) { return area; } } // Recursively, resolve the parent areas Area parent = null; if (templateArea.getParent() != null) { parent = findOrCreateArea(templateArea.getParent(), create); if (!create && parent == null) { return null; } } Integer parentId = (parent == null) ? null : parent.getId(); // Check if we can find the given area Area area = null; for (int x = 0; area == null && x < templateArea.getDescs().size(); x++) { AreaDesc desc = templateArea.getDescs().get(x); area = findByName(desc.getName(), desc.getLang(), parentId); } // Create the area if no matching area was found if (create && area == null) { area = createArea(templateArea, parentId); } return area; } /** * Returns the last change date for areas or null if no area exists * @return the last change date for areas */ public Date getLastUpdated() { try { return em.createNamedQuery("Area.findLastUpdated", Date.class).getSingleResult(); } catch (Exception e) { return null; } } /** * Called periodically every hour to re-sort the area tree * * Potentially, a heavy-duty function that scans the entire area tree, * sorts it and update the treeSortOrder. Use with care. * * @return if the sort order was updated */ @Schedule(persistent = false, second = "3", minute = "13", hour = "*") public boolean recomputeTreeSortOrder() { long t0 = System.currentTimeMillis(); // Compare the last area update date and the last processed date Date lastAreaUpdate = getLastUpdated(); if (lastAreaUpdate == null) { // No areas return false; } Date lastProcessedUpdate = settingsService.getDate(new Setting(SETTING_AREA_LAST_UPDATED, null, false)); if (lastProcessedUpdate == null) { lastProcessedUpdate = new Date(0); } if (!lastAreaUpdate.after(lastProcessedUpdate)) { log.debug("No area tree changes since last execution of recomputeTreeSortOrder()"); return false; } // Get root areas (sorted) List<Area> roots = em.createNamedQuery("Area.findRootAreas", Area.class).getResultList(); // Re-compute the tree sort order List<Area> updated = new ArrayList<>(); recomputeTreeSortOrder(roots, 0, updated, false, false); // Persist changed areas updated.forEach(this::saveEntity); em.flush(); // Update the last processed date settingsService.setDate(SETTING_AREA_LAST_UPDATED, lastAreaUpdate); log.info("Recomputed tree sort order in " + (System.currentTimeMillis() - t0) + " ms"); return updated.size() > 0; } /** * Recursively recomputes the "treeSortOrder", by enumerating the sorted area list and their children. * The "treeSortOrder" is used for sorting messages by their area. If an area specifies the "messageSorting" * field, then all sub-areas of this are will have the same treeSortOrder. * * @param areas the list of areas to update * @param index the current area index * @param updatedAreas the list of updated areas given by sub-tree roots. * @param ancestorUpdated if an ancestor area has been updated * @param messageSorting whether this sub-tree is ordered via the "messageSorting" parameter or not * @return the index after processing the list of areas. */ private int recomputeTreeSortOrder(List<Area> areas, int index, List<Area> updatedAreas, boolean ancestorUpdated, boolean messageSorting) { for (Area area : areas) { if (!messageSorting) { index++; } boolean areaMessageSorting = messageSorting || area.getMessageSorting() != null; boolean updated = ancestorUpdated; if (index != area.getTreeSortOrder()) { area.setTreeSortOrder(index); updated = true; if (!ancestorUpdated) { updatedAreas.add(area); } } // NB: area.getChildren() is by definition sorted (by "siblingSortOrder") index = recomputeTreeSortOrder(area.getChildren(), index, updatedAreas, updated, areaMessageSorting); } return index; } /***************************************/ /** Message Area Sorting **/ /***************************************/ /** * Generate a tentative sorting order for the message within its associated area. * The area-sort value is based on the message center latitude and longitude, and the sorting type for * its first associated area. * <p> * The original algorithm was used in the DMA MSIadmin web application, and implemented by * {@code dk.frv.msiedit.core.domain.Location.generateSortingOrder()} * * @return a sorting order */ public double computeMessageAreaSortingOrder(Message message) { double no = 0.0; // Sanity check if (message.getAreas().isEmpty()) { return no; } // Compute the message center double[] center = GeoJsonUtils.computeCenter(message.toGeoJson()); if (center == null) { return no; } double lat = center[1]; double lon = center[0]; // Find parent area with a "messageSorting" definition Area area = message.getAreas().get(0); while (area != null && area.getMessageSorting() == null) { area = area.getParent(); } if (area == null) { return no; } AreaMessageSorting sortType = area.getMessageSorting(); switch (sortType) { case NS: no = -lat; break; case SN: no = lat; break; case EW: no = -lon; break; case WE: no = lon; break; case CW: case CCW: no = computeCwOrCcwSortOrder(area, lat, lon); break; } // Each sort number must be different no += new Random().nextDouble() / 1000000.0; return no; } /** Calculates the message area sort order for CW and CCW types **/ private double computeCwOrCcwSortOrder(Area area, double lat, double lon) { double no = 0.0; if (area.getOriginLatitude() == null || area.getOriginLongitude() == null) { return no; } double x = lon2x(area.getOriginLongitude(), area.getOriginLatitude(), lon, lat); double y = lat2y(area.getOriginLongitude(), area.getOriginLatitude(), lon, lat); double ang = 0.0; if (x == 0.0 && y > 0.0) { ang = 90.0; } else if (x == 0.0 && y < 0.0) { ang = 270.0; } else if (x != 0.0) { ang = Math.atan(y / x) * 180 / Math.PI; if (x < 0.0) { ang += 180.0; } else if (x > 0.0 && y < 0.0) { ang += 360.0; } } no = ang - (area.getOriginAngle() == null ? 0 : area.getOriginAngle()); if (no < 0.0) { no += 360.0; } return (area.getMessageSorting() == AreaMessageSorting.CW) ? -no : no; } /** calculates the horizontal distance from lon0 to lon **/ private double lon2x(double lon0, double lat0, double lon, double lat) { double radius = 6356752.3; //Radius of the sphere. double deg2Rad = 180.0 / Math.PI; double lon_rad = lon / deg2Rad; double lat_rad = lat / deg2Rad; double lon0_rad = lon0 / deg2Rad; double lat0_rad = lat0 / deg2Rad; double x = 0.0; double denom = (1.0 + Math.sin(lat0_rad) * Math.sin(lat_rad) + Math.cos(lat0_rad) * Math.cos(lat_rad) * Math.cos(lon_rad - lon0_rad)); if (denom != 0.0) { x = ((2.0 * radius) / denom) * Math.cos(lat_rad) * Math.sin(lon_rad - lon0_rad); } return x; } /** calculates the vertical distance from lon0 to lon **/ private double lat2y(double lon0, double lat0, double lon, double lat) { double radius = 6356752.3; //Radius of the sphere. double deg2Rad = 180.0 / Math.PI; double lon_rad = lon / deg2Rad; double lat_rad = lat / deg2Rad; double lon0_rad = lon0 / deg2Rad; double lat0_rad = lat0 / deg2Rad; double y = 0.0; double denom = (1.0 + Math.sin(lat0_rad) * Math.sin(lat_rad) + Math.cos(lat0_rad) * Math.cos(lat_rad) * Math.cos(lon_rad - lon0_rad)); if (denom != 0.0) { y = ((2.0 * radius) / denom) * (Math.cos(lat0_rad) * Math.sin(lat_rad) - Math.sin(lat0_rad) * Math.cos(lat_rad) * Math.cos(lon_rad - lon0_rad)); } return y; } }