/*******************************************************************************
* Copyright (c) 2014 Open Door Logistics (www.opendoorlogistics.com)
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Lesser Public License v3
* which accompanies this distribution, and is available at http://www.gnu.org/licenses/lgpl.txt
******************************************************************************/
package com.opendoorlogistics.components.geocode.postcodes;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.io.File;
import java.io.Serializable;
import java.util.List;
import javax.swing.Icon;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextPane;
import com.opendoorlogistics.api.ODLApi;
import com.opendoorlogistics.api.components.ComponentConfigurationEditorAPI;
import com.opendoorlogistics.api.components.ComponentControlLauncherApi;
import com.opendoorlogistics.api.components.ComponentControlLauncherApi.ControlLauncherCallback;
import com.opendoorlogistics.api.components.ComponentExecutionApi;
import com.opendoorlogistics.api.components.ODLComponent;
import com.opendoorlogistics.api.components.PredefinedTags;
import com.opendoorlogistics.api.scripts.ScriptTemplatesBuilder;
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.TableFlags;
import com.opendoorlogistics.api.ui.Disposable;
import com.opendoorlogistics.components.geocode.Countries.Country;
import com.opendoorlogistics.components.geocode.postcodes.builder.GDFFileBuilder;
import com.opendoorlogistics.components.geocode.postcodes.builder.CodePointOpen2GeonamesFormat;
import com.opendoorlogistics.components.geocode.postcodes.impl.PCConstants;
import com.opendoorlogistics.components.geocode.postcodes.impl.PCGeocodeFile;
import com.opendoorlogistics.components.geocode.postcodes.impl.PCGeocodeFile.PCFindResult;
import com.opendoorlogistics.components.geocode.postcodes.impl.PCRecord;
import com.opendoorlogistics.components.geocode.postcodes.impl.PCRecord.StrField;
import com.opendoorlogistics.core.CommandLineInterface;
import com.opendoorlogistics.core.CommandLineInterface.Command;
import com.opendoorlogistics.core.tables.utils.TableUtils;
import com.opendoorlogistics.core.utils.strings.Strings;
import com.opendoorlogistics.utils.ui.Icons;
public class PCGeocoderComponent implements ODLComponent {
static{
// register the builder commands lines
CommandLineInterface.registerCommand(new Command() {
@Override
public String[] getKeywords() {
return new String[]{"cp2Geonames"};
}
@Override
public String getDescription() {
return "Convert a directory of CodePoint postcode files to a single Geonames format file. Usage -cp2Geonames inputDirectory outputFileName";
}
@Override
public boolean execute(String[] args) {
if(args.length!=2){
System.out.println("Expected two arguments");
return false;
}
CodePointOpen2GeonamesFormat.process(args[0], args[1]);
return true;
}
});
CommandLineInterface.registerCommand(new Command() {
@Override
public String[] getKeywords() {
return new String[]{"buildgdf"};
}
@Override
public String getDescription() {
return "Build one or more postcode geocoding data files from the input Geonames format file. Usage -buildgdf inputfile outputdirectory";
}
@Override
public boolean execute(String[] args) {
if(args.length!=2){
System.out.println("Expected two arguments");
return false;
}
try {
new GDFFileBuilder().buildFromGeonamesFile(args[0], new String[]{}, true, args[1]);
return true;
} catch (Exception e) {
System.out.println(Strings.getExceptionMessagesAsSingleStr(e));
return false;
}
}
});
}
@Override
public String getId() {
return "com.opendoorlogistics.components.geocode.postcodegeocoder";
}
@Override
public String getName() {
return "Geocode postcodes";
}
@Override
public Class<? extends Serializable> getConfigClass() {
return PCGeocoderConfig.class;
}
@Override
public ODLDatastore<? extends ODLTableDefinition> getIODsDefinition(ODLApi api,Serializable configuration) {
ODLDatastoreAlterable< ? extends ODLTableDefinitionAlterable> ret =api.tables().createDefinitionDs();
ret.createTable("Postcodes",-1);
ODLTableDefinitionAlterable dfn = ret.getTableAt(0);
TableUtils.addColumn(dfn,"Postcode", ODLColumnType.STRING, 0,null,PredefinedTags.POSTCODE);
TableUtils.addColumn(dfn,PredefinedTags.LATITUDE, ODLColumnType.DOUBLE, 0,null, PredefinedTags.LATITUDE);
TableUtils.addColumn(dfn,PredefinedTags.LONGITUDE, ODLColumnType.DOUBLE, 0,null, PredefinedTags.LONGITUDE);
TableUtils.addColumn(dfn, "MatchInformation", ODLColumnType.STRING, TableFlags.FLAG_IS_OPTIONAL, "Information on the match or reason for failure");
//TableUtils.addColumn(dfn, name, type, flags, description, tags)
dfn.addColumn(-1,"IsMatched", ODLColumnType.STRING, TableFlags.FLAG_IS_OPTIONAL);
for(StrField fld : StrField.values()){
if(TableUtils.addColumn(dfn, "Matched"+fld.getDisplayText(), ODLColumnType.STRING, TableFlags.FLAG_IS_OPTIONAL ,null, fld.getTags())==-1){
throw new RuntimeException();
}
}
return ret;
}
@Override
public ODLDatastore<ODLTableDefinition> getOutputDsDefinition(ODLApi api,int mode, Serializable configuration) {
return null;
}
private boolean isStrictRejection(PCFindResult result, PCGeocoderConfig pgc, String [] reason){
if(pgc.isStrictMatch()){
if(result.getList().size()>1){
reason[0] = "Rejected as strict matching is on and matched to more than one postcode record.";
return true;
}
if(result.getLevel() < pgc.getMinimumLevel()){
reason[0] = "Rejected as matched to a postcode less than the minimum level.";
return true;
}
}
return false;
}
@Override
public void execute(ComponentExecutionApi api,int mode,Object configuration, ODLDatastore<? extends ODLTable> input, ODLDatastoreAlterable<? extends ODLTableAlterable> output) {
final PCGeocoderConfig pgc = (PCGeocoderConfig)configuration;
// get the file, if its not absolute then assume its in the installation directory
File file = PCConstants.resolvePostcodeFile(api.getApi(), new File(pgc.geocoderDbFilename));
PCGeocodeFile pc = new PCGeocodeFile(file);
class Counter{
int nbMatched=0;
int nbUnmatched=0;
int nbSkipped=0;
int nr;
int nbRejectedStrictMatch=0;
int nbIncorrectFormat=0;
int nbNoMatchesFound=0;
}
final Counter counter = new Counter();
final Country country = pc.getCountry();
try {
ODLTable table = input.getTableAt(0);
counter.nr = table.getRowCount();
for(int row =0 ; row < counter.nr ; row++){
// check whether to skip
if(pgc.isSkipAlreadyGeocodedRecords() && table.getValueAt(row, 1)!=null && table.getValueAt(row, 2)!=null){
counter.nbSkipped++;
continue;
}
String pcValue = (String)table.getValueAt(row, 0);
PCRecord matched=null;
String []reason=new String[1];
if(pcValue!=null){
PCFindResult result = pc.find(pcValue);
List<PCRecord> list = result.getList();
if(list!=null && list.size()>0){
if(isStrictRejection(result, pgc, reason)){
matched = null;
counter.nbRejectedStrictMatch++;
}else{
// merge matched results
matched = PCRecord.merge(list);
counter.nbMatched++;
int col=1;
table.setValueAt(matched.getLatitude(), row, col++);
table.setValueAt(matched.getLongitude(), row, col++);
if(list.size()>1){
table.setValueAt("Matched to " + list.size() + " postcodes in level " + result.getLevel(), row, col++);
}
else{
table.setValueAt("Matched to postcode " + matched.getField(StrField.POSTAL_CODE) + " in level " + result.getLevel(), row, col++);
}
table.setValueAt("1", row, col++);
for(StrField fld :StrField.values()){
table.setValueAt(matched.getField(fld), row, col++);
}
}
}else{
switch (result.getType()) {
case INVALID_FORMAT:
reason[0] = "Could not identify input postcode format";
counter.nbIncorrectFormat++;
break;
case NO_MATCHES:
reason[0] = "Input postcode format was correct but no matches found";
counter.nbNoMatchesFound++;
break;
default:
// this should never be called...
throw new RuntimeException();
}
}
}else{
reason[0] = "No input postcode provided";
}
// blank if not matched and record reason
if(matched==null){
counter.nbUnmatched++;
int col=1;
// set lat and long values to null
table.setValueAt(null, row, col++);
table.setValueAt(null, row, col++);
table.setValueAt(reason[0], row, col++);
// set isMatched to 0
table.setValueAt("0", row, col++);
// blank the string fields
for(int i =0 ; i < StrField.values().length ; i++){
table.setValueAt(null, row, col++);
}
}
}
}
finally{
pc.close();
}
// show results summary after script has run
if(pgc.isShowSummary()){
api.submitControlLauncher(new ControlLauncherCallback() {
@Override
public void launchControls(ComponentControlLauncherApi launcherApi) {
// build up text
StringBuilder builder = new StringBuilder();
builder.append("PC Geocoder using file: " + pgc.getGeocoderDbFilename() + System.lineSeparator());
if(country!=null){
builder.append("Country: " + country.getName());
}
builder.append(System.lineSeparator());
builder.append("Number of records skipped: " + counter.nbSkipped + System.lineSeparator());
builder.append("Number of records processed: " + (counter.nr-counter.nbSkipped) + System.lineSeparator());
builder.append("Number of records matched: " + counter.nbMatched + System.lineSeparator());
builder.append("Number of records unmatched: " + counter.nbUnmatched + System.lineSeparator());
if(pgc.isStrictMatch()){
builder.append("Number of records unmatched due to strict matching: " + counter.nbRejectedStrictMatch + System.lineSeparator());
}
builder.append("Number of records unmatched due to incorrect postcode format: " + counter.nbIncorrectFormat + System.lineSeparator());
builder.append("Number of records unmatched due to no matches found: " + counter.nbNoMatchesFound + System.lineSeparator());
// create panel to report it
JTextPane textPane = new JTextPane();
textPane.setText(builder.toString());
JScrollPane scroller = new JScrollPane(textPane);
class MyPanel extends JPanel implements Disposable{
@Override
public void dispose() {
// TODO Auto-generated method stub
}
}
MyPanel panel = new MyPanel();
panel.setLayout(new BorderLayout());
panel.add(scroller,BorderLayout.CENTER);
panel.setPreferredSize(new Dimension(400, 200));
launcherApi.registerPanel("PCSummary",null, panel, false);
}
});
}
}
@Override
public JPanel createConfigEditorPanel(ComponentConfigurationEditorAPI factory,int mode,Serializable config, boolean isFixedIO) {
return new PCGeocoderConfigPanel(factory.getApi(),(PCGeocoderConfig)config);
}
@Override
public long getFlags(ODLApi api,int mode) {
return 0;
}
// @Override
// public Iterable<ODLWizardTemplateConfig> getWizardTemplateConfigs(ODLApi api) {
// return Arrays.asList(
// new ODLWizardTemplateConfig(getName(), getName(), "Batch geocoding of a table using postcode/zipcode available from www.geonames.org.", new PCDatabaseSelectionConfig())
//
// // new ODLWizardTemplateConfig("GB" + getName(), "GB" + getName(), "Batch geocoding of a table using postcode/zipcode available from www.geonames.org.", new PCDatabaseSelectionConfig("gb.gdf"))
//
// );
// }
@Override
public void registerScriptTemplates(ScriptTemplatesBuilder templatesApi) {
templatesApi.registerTemplate(getName(), getName(), "Batch geocoding of a table using postcode/zipcode available from www.geonames.org.",
getIODsDefinition(templatesApi.getApi(), new PCGeocoderConfig()),
new PCGeocoderConfig());
}
@Override
public Icon getIcon(ODLApi api,int mode) {
return Icons.loadFromStandardPath("postcode-geocode.png");
}
@Override
public boolean isModeSupported(ODLApi api,int mode) {
return mode==ODLComponent.MODE_DEFAULT;
}
}