package com.opendoorlogistics.components.heatmap; import java.io.Serializable; import java.util.ArrayList; import java.util.List; import javax.swing.BoxLayout; import javax.swing.Icon; import javax.swing.JPanel; import com.opendoorlogistics.api.ODLApi; import com.opendoorlogistics.api.StringConventions; import com.opendoorlogistics.api.components.ComponentConfigurationEditorAPI; import com.opendoorlogistics.api.components.ComponentExecutionApi; import com.opendoorlogistics.api.components.ODLComponent; import com.opendoorlogistics.api.scripts.ScriptAdapter; import com.opendoorlogistics.api.scripts.ScriptAdapterTable; import com.opendoorlogistics.api.scripts.ScriptInstruction; import com.opendoorlogistics.api.scripts.ScriptOption; import com.opendoorlogistics.api.scripts.ScriptTemplatesBuilder; import com.opendoorlogistics.api.standardcomponents.Maps; import com.opendoorlogistics.api.tables.ODLColumnType; 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.ODLTableReadOnly; import com.opendoorlogistics.api.tables.TableFlags; import com.opendoorlogistics.api.ui.Disposable; import com.opendoorlogistics.api.ui.UIFactory; import com.opendoorlogistics.api.ui.UIFactory.DoubleChangedListener; import com.opendoorlogistics.api.ui.UIFactory.IntChangedListener; import com.opendoorlogistics.api.ui.UIFactory.TextChangedListener; import com.opendoorlogistics.components.heatmap.HeatmapGenerator.HeatMapResult; import com.opendoorlogistics.components.heatmap.HeatmapGenerator.InputPoint; import com.opendoorlogistics.components.heatmap.HeatmapGenerator.SingleContourGroup; import com.opendoorlogistics.core.geometry.ODLLoadedGeometry; import com.opendoorlogistics.core.geometry.operations.GridTransforms; import com.opendoorlogistics.core.utils.Colours; import com.opendoorlogistics.core.utils.LargeList; import com.opendoorlogistics.utils.ui.Icons; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.Envelope; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryFactory; import com.vividsolutions.jts.geom.Point; public class HeatmapComponent implements ODLComponent { private static final Icon ICON = Icons.loadFromStandardPath("heatmap.png"); @Override public String getId() { return "com.opendoorlogistics.components.heatmap"; } @Override public String getName() { return "Heatmap from points"; } @Override public ODLDatastore<? extends ODLTableDefinition> getIODsDefinition(ODLApi api, Serializable configuration) { ODLDatastoreAlterable<?extends ODLTableDefinitionAlterable> ds = api.tables().createDefinitionDs(); ODLTableDefinitionAlterable table = ds.createTable("Points", -1); table.addColumn(-1, "Latitude", ODLColumnType.DOUBLE, 0); table.addColumn(-1, "Longitude", ODLColumnType.DOUBLE, 0); table.setColumnDefaultValue(table.addColumn(-1, "Weight", ODLColumnType.DOUBLE, TableFlags.FLAG_IS_OPTIONAL), 1.0); return ds; } @Override public ODLDatastore<? extends ODLTableDefinition> getOutputDsDefinition(ODLApi api, int mode, Serializable configuration) { ODLDatastoreAlterable<?extends ODLTableDefinitionAlterable> ds = api.tables().createDefinitionDs(); ODLTableDefinitionAlterable table = ds.createTable("ContourPolygons", -1); table.addColumn(-1, "Level", ODLColumnType.LONG, 0); table.addColumn(-1, "Min", ODLColumnType.DOUBLE, 0); table.addColumn(-1, "Max", ODLColumnType.DOUBLE, 0); table.addColumn(-1, "Colour", ODLColumnType.COLOUR, 0); table.addColumn(-1, "Geometry", ODLColumnType.GEOM, 0); return ds; } @Override public void execute(ComponentExecutionApi api, int mode, Object configuration, ODLDatastore<? extends ODLTable> ioDs, ODLDatastoreAlterable<? extends ODLTableAlterable> outputDs) { HeatMapConfig c = (HeatMapConfig)configuration; GridTransforms transform = null; StringConventions sc = api.getApi().stringConventions(); if(!sc.isEmptyString(sc.standardise(c.getEPSG()))){ transform = GridTransforms.getAndCache(sc.standardise(c.getEPSG())); } GeometryFactory factory = new GeometryFactory(); // get input points array ODLTableReadOnly table = ioDs.getTableAt(0); List<InputPoint> points = new ArrayList<InputPoint>(); Envelope envelope = new Envelope(); int n = table.getRowCount(); for(int i =0 ; i < n ; i++){ Double lat = (Double)table.getValueAt(i, 0); Double lng = (Double)table.getValueAt(i, 1); Double weight = (Double)table.getValueAt(i, 2); if(weight==null){ weight= 1.0; } if(lat!=null && lng!=null){ Coordinate coordinate = new Coordinate(lng, lat); Point point = factory.createPoint(coordinate); if(transform!=null){ point = (Point)transform.wgs84ToGrid(point); } envelope.expandToInclude(point.getCoordinate()); points.add(new InputPoint(point, weight)); } } // check for empty if(envelope.isNull() || points.size()==0){ return; } // choose the area based on points having no effect outside it envelope.expandBy(c.getPointRadius() * 2 * HeatmapGenerator.GAUSSIAN_CUTOFF); // get cell length based on resolution double maxDim = Math.max(envelope.getHeight(), envelope.getWidth()); double cellLength = maxDim / c.getResolution(); // create result HeatMapResult result = HeatmapGenerator.build(points, c.getPointRadius(), envelope, cellLength, c.getNbContourLevels(),api); if(api.isCancelled()){ return; } // write out ODLTable outTable = outputDs.getTableAt(0); for(SingleContourGroup r : result.groups){ if(r.geometry!=null){ int row = outTable.createEmptyRow(-1); outTable.setValueAt(r.level+1, row, 0); outTable.setValueAt(result.levelLowerLimits[r.level], row, 1); outTable.setValueAt(result.levelUpperLimits[r.level], row, 2); outTable.setValueAt(Colours.temperature((double)r.level / (c.getNbContourLevels()+1)), row, 3); Geometry p = r.geometry; if(transform!=null){ p = transform.gridToWGS84(p); } outTable.setValueAt(new ODLLoadedGeometry(p) ,row, 4); } } } @Override public Class<? extends Serializable> getConfigClass() { return HeatMapConfig.class; } @Override public JPanel createConfigEditorPanel(ComponentConfigurationEditorAPI api, int mode, Serializable config, boolean isFixedIO) { HeatMapConfig c = (HeatMapConfig)config; UIFactory uiFactory = api.getApi().uiFactory(); class MyPanel extends JPanel implements Disposable{ @Override public void dispose() { // TODO Auto-generated method stub } } MyPanel ret = new MyPanel(); ret.setLayout(new BoxLayout(ret, BoxLayout.Y_AXIS)); ret.add(uiFactory.createTextEntryPane("EPSG projection ", c.getEPSG(), "EPSG projection code or empty if using latitude and longitude", new TextChangedListener() { @Override public void textChange(String newText) { c.setEPSG(newText); } })); ret.add(uiFactory.createDoubleEntryPane("Radius of point influence ", c.getPointRadius(), "Radius of point influence (in projected units or degrees if unprojected), used as standard deviation in Gaussian equation", new DoubleChangedListener() { @Override public void doubleChange(double newValue) { c.setPointRadius(newValue); } })); ret.add(uiFactory.createIntegerEntryPane("Number of contour levels " , c.getNbContourLevels(), "Number of contour levels", new IntChangedListener() { @Override public void intChange(int newInt) { c.setNbContourLevels(newInt); } })); ret.add(uiFactory.createIntegerEntryPane("Calculation resolution " , c.getResolution(), "Calculation resolution - higher resolutions take longer to calculate", new IntChangedListener() { @Override public void intChange(int newInt) { c.setResolution(newInt); } })); // JCheckBox checkBox = new JCheckBox("Smooth polygon edges", c.isSimplify()); // checkBox.addActionListener(new ActionListener() { // // @Override // public void actionPerformed(ActionEvent e) { // c.setSimplify(checkBox.isSelected()); // } // }); // ret.add(checkBox); return ret; } @Override public long getFlags(ODLApi api, int mode) { return ODLComponent.FLAG_ALLOW_USER_INTERACTION_WHEN_RUNNING; } @Override public Icon getIcon(ODLApi api, int mode) { return ICON; } @Override public boolean isModeSupported(ODLApi api, int mode) { return mode == ODLComponent.MODE_DEFAULT; } @Override public void registerScriptTemplates(ScriptTemplatesBuilder templatesApi) { templatesApi.registerTemplate("Heatmap", getName(), "Generate heatmap contour polygons from input points with weights.",getIODsDefinition(templatesApi.getApi(), new HeatMapConfig()), new ScriptTemplatesBuilder.BuildScriptCallback() { @Override public void buildScript(ScriptOption builder) { ODLApi api = templatesApi.getApi(); // Add instruction to generate the heatmap HeatMapConfig config = new HeatMapConfig(); ScriptAdapter input = builder.addDataAdapterLinkedToInputTables("heatmapinput", getIODsDefinition(api, config)); ScriptInstruction instruction = builder.addInstruction(input.getAdapterId(), getId(), ODLComponent.MODE_DEFAULT, config); instruction.setOutputDatastoreId(builder.createUniqueDatastoreId("CountourPolygons")); // add showmap option ScriptOption showMapOption = builder.addOption("Show heatmap", "Show heatmap"); showMapOption.setSynced(false); ODLTableDefinition heatmapOutTable= getOutputDsDefinition(api, ODLComponent.MODE_DEFAULT, config).getTableAt(0); ScriptAdapter mapInput = showMapOption.addDataAdapter("Heatmap map"); Maps maps = api.standardComponents().map(); ScriptAdapterTable drawableTable = mapInput.addSourcelessTable(maps.getDrawableTableDefinition()); drawableTable.setSourceTable(instruction.getOutputDatastoreId(), heatmapOutTable.getName()); drawableTable.setSourceColumn("geometry", "Geometry"); drawableTable.setSourceColumn("colour", "Colour"); //drawableTable.setSourceColumn("legendKey", "Level"); String legendFormula ="c(\"Level \") & Level & \" (\" & stringformat(\"%.3f\", min) & \"-\" & stringformat(\"%.3f\", max) & \")\"";// "level & " = "min & \" <= density < \" & max"; drawableTable.setFormula("legendKey",legendFormula); drawableTable.setFormula("tooltip", legendFormula); drawableTable.setFormula("nonOverlappingPolygonLayerGroupKey", "\"yes\""); drawableTable.setFormula("opaque", "0.35"); Serializable mapConfig; try { mapConfig = maps.getConfigClass().newInstance(); } catch (Exception e) { throw new RuntimeException(e); } maps.setCustomTooltips(true, mapConfig); showMapOption.addInstruction(mapInput.getAdapterId(), maps.getId(), ODLComponent.MODE_DEFAULT, mapConfig); // add export option ScriptOption exportOutput = builder.addOption("Export contour polygons", "Export contour polygons"); String outTableName = heatmapOutTable.getName(); exportOutput.addCopyTable(instruction.getOutputDatastoreId(), outTableName, ScriptOption.OutputType.REPLACE_CONTENTS_OF_EXISTING_TABLE, outTableName); exportOutput.setSynced(false); } }); } }