package com.opendoorlogistics.components.geocode.postcodes; import java.io.File; import java.io.Serializable; import java.util.Collections; import java.util.List; import javax.swing.Icon; import javax.swing.JPanel; import com.opendoorlogistics.api.ODLApi; import com.opendoorlogistics.api.Tables; import com.opendoorlogistics.api.cache.ObjectCache; import com.opendoorlogistics.api.components.ComponentConfigurationEditorAPI; import com.opendoorlogistics.api.components.ComponentExecutionApi; import com.opendoorlogistics.api.components.ODLComponent; import com.opendoorlogistics.api.geometry.ODLGeom; import com.opendoorlogistics.api.scripts.ScriptTemplatesBuilder; import com.opendoorlogistics.api.tables.ODLDatastore; import com.opendoorlogistics.api.tables.ODLDatastoreAlterable; import com.opendoorlogistics.api.tables.ODLTable; import com.opendoorlogistics.api.tables.ODLTableAlterable; import com.opendoorlogistics.api.tables.ODLTableDefinition; import com.opendoorlogistics.api.tables.ODLTableDefinitionAlterable; import com.opendoorlogistics.api.tables.beans.BeanMappedRow; import com.opendoorlogistics.api.tables.beans.BeanTableMapping; import com.opendoorlogistics.api.tables.beans.annotations.ODLIgnore; import com.opendoorlogistics.api.tables.beans.annotations.ODLNullAllowed; import com.opendoorlogistics.api.tables.beans.annotations.ODLTableName; import com.opendoorlogistics.components.geocode.postcodes.impl.PCConstants; import com.opendoorlogistics.components.geocode.postcodes.impl.PCGeocodeFile; import com.opendoorlogistics.components.geocode.postcodes.impl.PCRecord; import com.opendoorlogistics.components.geocode.postcodes.impl.PCRecord.StrField; import com.opendoorlogistics.core.geometry.ODLGeomImpl; import com.opendoorlogistics.core.geometry.operations.FastContainedPointsQuadtree; import com.opendoorlogistics.core.geometry.operations.FastContainedPointsQuadtree.Builder.InsertedListener; import com.opendoorlogistics.core.utils.LargeList; import com.opendoorlogistics.core.utils.strings.Strings; import com.opendoorlogistics.utils.ui.Icons; import com.sun.management.GcInfo; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryFactory; import gnu.trove.procedure.TLongProcedure; import gnu.trove.set.hash.TLongHashSet; public class PCSpatialQueryComponent implements ODLComponent{ public static class BaseIOClass implements BeanMappedRow{ private long globalRowId; private String polygonId; @Override public long getGlobalRowId() { return globalRowId; } @Override @ODLIgnore public void setGlobalRowId(long globalRowId) { this.globalRowId = globalRowId; } public String getPolygonId() { return polygonId; } @ODLNullAllowed public void setPolygonId(String polygonId) { this.polygonId = polygonId; } } @ODLTableName("Polygons") public static class Input extends BaseIOClass{ private ODLGeom geom; public ODLGeom getGeom() { return geom; } @ODLNullAllowed public void setGeom(ODLGeom geom) { this.geom = geom; } } @ODLTableName("Postcodes") public static class Output extends BaseIOClass implements Comparable<Output>{ private String postcode; public String getPostcode() { return postcode; } public void setPostcode(String postcode) { this.postcode = postcode; } @Override public int compareTo(Output o) { int diff = Strings.compareStd(getPolygonId(), o.getPolygonId(), false); if(diff==0){ diff = Strings.compareStd(getPostcode(), o.getPostcode(), false); } return diff; } } @Override public String getId() { return "com.opendoorlogistics.components.geocode.postcodes.PCSpatialQueryComponent"; } @Override public String getName() { return "Query for postcodes within polygons"; } @Override public ODLDatastore<? extends ODLTableDefinition> getIODsDefinition(ODLApi api, Serializable configuration) { return getClsDefn(api, Input.class); } private ODLDatastore<? extends ODLTableDefinition> getClsDefn(ODLApi api,Class<? extends BeanMappedRow> cls){ Tables tables = api.tables(); ODLDatastoreAlterable<? extends ODLTableDefinitionAlterable> ret = tables.createAlterableDs(); tables.copyTableDefinition(tables.mapBeanToTable(cls).getTableDefinition(), ret); return ret; } @Override public ODLDatastore<? extends ODLTableDefinition> getOutputDsDefinition(ODLApi api, int mode, Serializable configuration) { return getClsDefn(api, Output.class); } private static class CachedTree{ FastContainedPointsQuadtree tree ; LargeList<String> list; long sizeInBytes(){ long ret=tree.getEstimatedSizeInBytes(); for(String s:list){ if(s!=null){ ret +=s.length()*2; } } return ret; } } private static CachedTree createQuadtree(ComponentExecutionApi api, File file, int level){ // do import and add to builder api.postStatusMessage("Loading postcodes from file " + file.getName()); PCGeocodeFile pc =null; try { // load all the postcodes pc = new PCGeocodeFile(file); FastContainedPointsQuadtree.Builder builder = new FastContainedPointsQuadtree.Builder(); CachedTree ret = new CachedTree(); ret.list= new LargeList<>(); long count=0; List<PCRecord> postcodes = pc.getPostcodes(level, api); if(api.isCancelled()){ return null; } // add them all to the builder for (PCRecord record : postcodes) { long id = ret.list.longSize(); builder.add(new Coordinate(record.getLongitude().doubleValue(), record.getLatitude().doubleValue()), id); ret.list.add(record.getField(StrField.POSTAL_CODE)); count++; if(count%25000==0){ api.postStatusMessage("Building postcode lookup tree step 1: processed " + count + " postcodes."); } } pc.close(); pc = null; if(api.isCancelled()){ return null; } // create tree api.postStatusMessage("Building postcode lookup tree"); GeometryFactory geometryFactory = new GeometryFactory(); ret.tree = builder.build(geometryFactory, new InsertedListener() { @Override public void inserted(Coordinate c, long count) { if(count%25000==0){ api.postStatusMessage("Building postcode lookup tree step 2: processed " + count + " postcodes."); } } }); return ret; } catch (Exception e) { throw e; }finally { if(pc!=null){ pc.close(); } } } private synchronized ObjectCache initCache(ODLApi api){ String id = getId() + " - treecache"; ObjectCache ret = api.cache().get(id); if(ret==null){ ret = api.cache().create(id, 1024 * 1024 * 256); } return ret; } /** * Query for postcodes contained within the input polygons. The query doesn't do any projection, so it won't work * for any input polygons which cross the 180th meridian, so it won't work for a small part of North Eastern Russia * (see https://en.wikipedia.org/wiki/180th_meridian) or a polygon spanning the poles (e.g. Antartica). */ @Override public void execute(ComponentExecutionApi api, int mode, Object configuration, ODLDatastore<? extends ODLTable> ioDs, ODLDatastoreAlterable<? extends ODLTableAlterable> outputDs) { // Get the filename PCImporterConfig config = (PCImporterConfig)configuration; File file = PCConstants.resolvePostcodeFile(api.getApi(), new File(config.getGeocoderDbFilename())); // Get a cache key including characters which should be illegal under any file system (windows, unix) String cacheKey = file.getAbsolutePath() + " \\/?/\\ " + config.getLevel(); // see if we have a quadtree cached for these postcodes... ObjectCache cache = initCache(api.getApi()); CachedTree tree = (CachedTree)cache.get(cacheKey); // load tree if needed, an exception will be thrown if loaded failed... if(tree==null){ tree = createQuadtree(api, file, config.getLevel()); if(api.isCancelled()){ return; } cache.put(cacheKey, tree, tree.sizeInBytes()); Runtime.getRuntime().gc(); } Tables tables = api.getApi().tables(); BeanTableMapping inputMapping = tables.mapBeanToTable(Input.class); List<Input> polygons = inputMapping.readObjectsFromTable(ioDs.getTableAt(0)); BeanTableMapping outputMapping = tables.mapBeanToTable(Output.class); TLongHashSet ids = new TLongHashSet(); int polyCount=0; for(Input polygon: polygons){ if(api.isCancelled() || api.isFinishNow()){ return; } api.postStatusMessage("Querying for polygon " + (polyCount+1)); if(polygon.getGeom()!=null){ Geometry geometry = ((ODLGeomImpl)polygon.getGeom()).getJTSGeometry(); if(geometry!=null){ ids.clear(); tree.tree.query(geometry, ids); CachedTree finalTree = tree; // get list of output objects LargeList<Output> outputList = new LargeList<>(); ids.forEach(new TLongProcedure() { @Override public boolean execute(long value) { Output output = new Output(); output.setPolygonId(polygon.getPolygonId()); output.setPostcode(finalTree.list.get(value)); outputList.add(output); return true; } }); // sort list Collections.sort(outputList); // write to table for(Output output : outputList){ if(api.isCancelled() || api.isFinishNow()){ return; } outputMapping.writeObjectToTable(output, outputDs.getTableAt(0)); } } } polyCount++; } } @Override public Class<? extends Serializable> getConfigClass() { return PCImporterConfig.class; } @Override public JPanel createConfigEditorPanel(ComponentConfigurationEditorAPI api, int mode, Serializable config, boolean isFixedIO) { return PCImporterConfig.createConfigEditorPanel(api,(PCImporterConfig) config, "Query"); } @Override public long getFlags(ODLApi api, int mode) { return ODLComponent.FLAG_ALLOW_USER_INTERACTION_WHEN_RUNNING |ODLComponent.FLAG_OUTPUT_WINDOWS_CAN_BE_SYNCHRONISED; } @Override public Icon getIcon(ODLApi api, int mode) { return Icons.loadFromStandardPath("postcode-spatial-query.png"); } @Override public boolean isModeSupported(ODLApi api, int mode) { return mode == ODLComponent.MODE_DEFAULT; } @Override public void registerScriptTemplates(ScriptTemplatesBuilder templatesApi) { templatesApi.registerTemplate(getName(), getName(), "Query for postcodes contained within a set of input polygons.", getIODsDefinition(templatesApi.getApi(), new PCImporterConfig()), new PCImporterConfig()); } }