diff --git a/.gitignore b/.gitignore index d53965e..1b2462f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ -app.config +log4net.xml out/ -_work_ +_work_/ .vscode/ *.swp diff --git a/Makefile b/Makefile index fcfcf58..0f9a011 100644 --- a/Makefile +++ b/Makefile @@ -10,9 +10,9 @@ compile: csc $(SRC) /nologo /warnaserror \ /r:lib/Newtonsoft.Json.dll \ /r:lib/ImageListView.dll \ + /r:lib/log4net.dll \ /d:TRACE \ /target:winexe /out:$(OUTPUT)/asl-charts.exe - cp app.config $(OUTPUT)/asl-charts.exe.config cp lib/*.dll $(OUTPUT) clean: diff --git a/_archive_/Json120r2.zip b/_archive_/Json120r2.zip old mode 100755 new mode 100644 diff --git a/_archive_/log4net-2.0.8-bin-newkey.zip b/_archive_/log4net-2.0.8-bin-newkey.zip new file mode 100644 index 0000000..c7bcb6c Binary files /dev/null and b/_archive_/log4net-2.0.8-bin-newkey.zip differ diff --git a/_archive_/log4net-2.0.8-src.zip b/_archive_/log4net-2.0.8-src.zip new file mode 100644 index 0000000..48c13d3 Binary files /dev/null and b/_archive_/log4net-2.0.8-src.zip differ diff --git a/data/config.json b/data/config.json index 5a6ba3f..fbd4c97 100644 --- a/data/config.json +++ b/data/config.json @@ -1,27 +1,33 @@ { "IIFTMQRDCc/leader-creation.png": { - "caption": "Leader Creation" + "caption": "Leader Creation", + "keywords": [ "LC", "leader" ] }, "IIFTMQRDCc/close-combat.png": { - "caption": "Close Combat" + "caption": "Close Combat", + "keywords": [ "CC" ] }, "IIFTMQRDCc/heat-to-kill.png": { - "caption": "HEAT To Kill" + "caption": "HEAT To Kill", + "keywords": [ "HEAT", "TK" ] }, "IIFTMQRDCc/iift.png": { - "caption": "Incremental IFT" + "caption": "Incremental IFT", + "keywords": [ "IFT", "IIFT" ] }, "IIFTMQRDCc/ap-to-kill.png": { - "caption": "AP To Kill" + "caption": "AP To Kill", + "keywords": [ "AP", "TK" ] }, "IIFTMQRDCc/afv-destruction.png": { - "caption": "AFV Destruction" + "caption": "AFV Destruction", + "keywords": [ "AFV" ] }, "IIFTMQRDCc/ambush.png": { @@ -29,15 +35,18 @@ }, "IIFTMQRDCc/heat-of-battle.png": { - "caption": "Heat Of Battle" + "caption": "Heat Of Battle", + "keywords": [ "HOB" ] }, "IIFTMQRDCc/apcr-apds-to-kill.png": { - "caption": "APCR/APDS To Kill" + "caption": "APCR/APDS To Kill", + "keywords": [ "APCR", "APDS", "TK" ] }, "IIFTMQRDCc/to-hit.png": { - "caption": "To Hit" + "caption": "To Hit", + "keywords": [ "TH" ] } } diff --git a/lib/log4net.dll b/lib/log4net.dll new file mode 100644 index 0000000..d7707b7 Binary files /dev/null and b/lib/log4net.dll differ diff --git a/src/ChartImage.cs b/src/ChartImage.cs index 2ace334..bf9bc71 100644 --- a/src/ChartImage.cs +++ b/src/ChartImage.cs @@ -1,7 +1,12 @@ +using System ; +using System.Text ; using System.IO ; +using System.Collections.Generic ; using System.Drawing ; using Manina.Windows.Forms ; +using Newtonsoft.Json.Linq ; +using log4net ; // -------------------------------------------------------------------- @@ -9,15 +14,25 @@ public class ChartImage { private string mFullPath ; private dynamic mConfig ; + private HashSet mKeywords = new HashSet() ; private Image mImage ; private ImageListViewItem mImageListViewItem ; public ChartImage( string key, string fullPath ) { + // initialize the ChartImage mFullPath = fullPath ; mConfig = Program.dataConfig.data[ key ] ; + if ( mConfig == null ) + mConfig = new JObject() ; mImage = Image.FromFile( fullPath ) ; + // prepare for search scoring + if ( mConfig["keywords"] != null ) { + foreach( string kywd in mConfig["keywords"] ) + mKeywords.Add( kywd.ToUpper() ) ; + } + // NOTE: Thumbnails are cached by ImageListViewItem GUID, so we reuse these objects, // instead of creating new ones every time we reload the search results. mImageListViewItem = new ImageListViewItem( fullPath ) ; @@ -25,6 +40,90 @@ public class ChartImage mImageListViewItem.Text = caption() ; } + public float getSearchScore( string searchQuery ) + { + // initialize + searchQuery = searchQuery.ToUpper() ; + List< Tuple > scores = new List>() ; + ILog logger = LogManager.GetLogger( "search" ) ; + if ( logger.IsDebugEnabled ) { + if ( mKeywords.Count == 0 ) + logger.Debug( $"- \"{this.caption()}\": (no keywords)" ) ; + else { + StringBuilder buf2 = new StringBuilder() ; + foreach( string kywd in mKeywords ) + buf2.Append( $"{kywd} ; " ) ; + string val = buf2.ToString() ; + logger.Debug( $"- \"{this.caption()}\": [ {val.Substring(0,val.Length-3)} ]" ) ; + } + } + + // initialize the search score weights + float exactKeywordMatchScore = Program.appConfig.getFloatVal( new string[]{"search","exactKeywordMatchScore"}, 10f ) ; + float leadingPartialKeywordMatchScore = Program.appConfig.getFloatVal( new string[]{"search","leadingPartialKeywordMatchScore"}, 2f ) ; + float internalPartialKeywordMatchScore = Program.appConfig.getFloatVal( new string[]{"search","internalPartialKeywordMatchScore"}, 1.5f ) ; + float exactCaptionMatchScore = Program.appConfig.getFloatVal( new string[]{"search","exactCaptionMatchScore"}, 5f ) ; + float leadingPartialCaptionMatchScore = Program.appConfig.getFloatVal( new string[]{"search","leadingPartialCaptionMatchScore"}, 1f ) ; + float internalPartialCaptionMatchScore = Program.appConfig.getFloatVal( new string[]{"search","internalPartialCaptionMatchScore"}, 0.5f ) ; + + // look for keyword matches + foreach ( string kywd in mKeywords ) { + if ( kywd == searchQuery ) { + scores.Add( new Tuple( "exactKeywordMatch", exactKeywordMatchScore ) ) ; + continue ; + } + int pos = kywd.IndexOf( searchQuery ) ; + if ( pos == 0 ) { + scores.Add( new Tuple( + $"leadingPartialKeywordMatch[{kywd}]", + Math.Min( leadingPartialKeywordMatchScore * searchQuery.Length, exactKeywordMatchScore ) + ) ) ; + } else if ( pos > 0 ) { + scores.Add( new Tuple( + $"internalPartialKeywordMatch[{kywd}]", + Math.Min( internalPartialKeywordMatchScore * searchQuery.Length, exactKeywordMatchScore ) + ) ) ; + } + } + + // look for caption matches + string caption = this.caption().ToUpper() ; + if ( searchQuery == caption ) + scores.Add( new Tuple( "exactCaptionMatch", exactCaptionMatchScore ) ) ; + else { + int pos = caption.IndexOf( searchQuery ) ; + if ( pos == 0 ) { + scores.Add( new Tuple( + $"leadingPartialCaptionMatch[{caption}]", + Math.Min( leadingPartialCaptionMatchScore * searchQuery.Length, exactCaptionMatchScore ) + ) ) ; + } else if ( pos > 0 ) { + scores.Add( new Tuple( + $"internalPartialCaptionMatch[{caption}]", + Math.Min( internalPartialCaptionMatchScore * searchQuery.Length, exactCaptionMatchScore ) + ) ) ; + } + } + + // calculate the total score + float totalScore = 0 ; + StringBuilder buf = logger.IsDebugEnabled ? new StringBuilder() : null ; + for ( int i=0 ; i < scores.Count ; ++i ) { + totalScore += scores[i].Item2 ; + if ( buf != null ) { + if ( i > 0 ) + buf.Append( " ; " ) ; + buf.Append( $"{scores[i].Item1}={scores[i].Item2:F1}" ) ; + } + } + if ( totalScore > 0 ) { + logger.Debug( $" - {buf}" ) ; + logger.Debug( $" - totalScore = {totalScore:F1}" ) ; + } + + return totalScore ; + } + public string caption() { string caption = (mConfig != null) ? mConfig["caption"] : null ; diff --git a/src/JsonConfig.cs b/src/JsonConfig.cs index 599bda9..3008136 100644 --- a/src/JsonConfig.cs +++ b/src/JsonConfig.cs @@ -2,6 +2,8 @@ using System ; using System.IO ; using Newtonsoft.Json ; +using Newtonsoft.Json.Linq ; +using log4net ; // -------------------------------------------------------------------- @@ -11,14 +13,52 @@ public class JsonConfig public JsonConfig( string caption, string fname ) { + // initialize + ILog logger = LogManager.GetLogger( "startup" ) ; + // load the JSON config string data ; if ( File.Exists( fname ) ) { - Program.logTraceMsg( String.Format( "Loading {0}: {1}", caption, Path.GetFullPath(fname) ) ) ; + logger.Info( $"Loading {caption}: {Path.GetFullPath(fname)}" ) ; data = File.ReadAllText( fname ) ; } else data ="{}" ; mData = JsonConvert.DeserializeObject( data ) ; + logger.Debug( mData.ToString() ) ; + } + + public string getStringVal( string[] keys, string defaultVal="" ) + { + // get the specified value + JToken curr = mData ; + foreach( string key in keys ) { + curr = curr[ key ] ; + if ( curr == null ) + return defaultVal ; + } + return (string) curr ; + } + + public int getIntVal( string[] keys, int defaultVal=0 ) + { + // get the specified value + string val = getStringVal( keys ) ; + try { + return Int32.Parse( val ) ; + } catch( FormatException ) { + return defaultVal ; + } + } + + public float getFloatVal( string[] keys, float defaultVal=0 ) + { + // get the specified value + string val = getStringVal( keys ) ; + try { + return float.Parse( val ) ; + } catch( FormatException ) { + return defaultVal ; + } } public dynamic data { get { return mData ; } } diff --git a/src/MainForm.cs b/src/MainForm.cs index 7906347..907fb8e 100644 --- a/src/MainForm.cs +++ b/src/MainForm.cs @@ -4,6 +4,7 @@ using System.Collections.Generic ; using System.Windows.Forms ; using Manina.Windows.Forms ; +using log4net ; // -------------------------------------------------------------------- @@ -28,6 +29,9 @@ public partial class MainForm : Form private void loadChartImages() { + // initialize + ILog logger = LogManager.GetLogger( "startup" ) ; + // locate the chart images string dataDir = Path.GetFullPath( Program.dataDir ) ; IEnumerable files = Directory.EnumerateFiles( @@ -48,21 +52,51 @@ public partial class MainForm : Form // their configuration using full paths for the image files, but at least we will still run... key = fullPath ; } - Program.logTraceMsg( String.Format( "Loading image: {0}", key ) ) ; + logger.Debug( $"Loading image: {key}" ) ; mChartImages[ key ] = new ChartImage( key, fullPath ) ; } } private void updateSearchResults( string searchQuery ) { + // initialize + searchQuery = searchQuery.Trim() ; + ILog logger = LogManager.GetLogger( "search" ) ; + logger.Info( $"Updating search results: query=\"{searchQuery}\"" ) ; + // search for matching chart images - List results = new List() ; - searchQuery = searchQuery.ToLower() ; - foreach ( ChartImage chartImage in mChartImages.Values ) { - if ( chartImage.caption().ToLower().IndexOf( searchQuery ) >= 0 ) - results.Add( chartImage ) ; + List< Tuple > results = new List>() ; + foreach( ChartImage chartImage in mChartImages.Values ) { + float score ; + if ( searchQuery == "" ) + score = 0 ; + else { + score = chartImage.getSearchScore( searchQuery ) ; + if ( score <= 0 ) + continue ; + } + results.Add( new Tuple( chartImage, score ) ) ; } - loadSearchResults( results ) ; + + // sort the search results + results.Sort( (lhs, rhs) => { + return (lhs.Item2 == rhs.Item2) ? 0 : (lhs.Item2 > rhs.Item2) ? -1 : +1 ; + } ) ; + if ( searchQuery != "" && logger.IsInfoEnabled ) { + if ( results.Count > 0 ) { + logger.Info( "- Sorted results:" ) ; + foreach ( var val in results ) + logger.Info( $" - \"{val.Item1.caption()}\" = {val.Item2:F1}" ) ; + } else + logger.Info( "- No results." ) ; + } else + logger.Info( "- No search query, showing all chart images." ) ; + + // show the search results + List results2 = new List() ; + foreach ( var r in results ) + results2.Add( r.Item1 ) ; + loadSearchResults( results2 ) ; } private void loadSearchResults( IEnumerable chartImages ) diff --git a/src/Program.cs b/src/Program.cs index 73fee2d..983576b 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -3,6 +3,8 @@ using System.Windows.Forms ; using System.IO ; using System.Diagnostics ; +using log4net.Config ; + // -------------------------------------------------------------------- public static class Program @@ -11,6 +13,7 @@ public static class Program private static string mBaseDir ; private static string mDataDir ; + private static JsonConfig mAppConfig = null ; private static JsonConfig mDataConfig = null ; private static JsonConfig mDebugConfig = null ; private static MainForm mMainForm = null ; @@ -21,10 +24,17 @@ public static class Program // initialize mBaseDir = Application.StartupPath ; + // configure logging + string fname = Path.Combine( mBaseDir, "log4net.xml" ) ; + if ( ! File.Exists( fname ) ) + fname = Path.Combine( mBaseDir, "../log4net.xml" ) ; + if ( File.Exists( fname ) ) + XmlConfigurator.Configure( new FileInfo( fname ) ) ; + // locate the data directory - mDataDir = System.IO.Path.Combine( mBaseDir , "data" ) ; + mDataDir = Path.Combine( mBaseDir , "data" ) ; if ( ! Directory.Exists( mDataDir ) ) { - mDataDir = System.IO.Path.Combine( mBaseDir , "../data" ) ; + mDataDir = Path.Combine( mBaseDir , "../data" ) ; if ( ! Directory.Exists( mDataDir ) ) mDataDir = mBaseDir ; } @@ -32,8 +42,16 @@ public static class Program try { // load the app config - // NOTE: It would be nice to be able to load the app config from another location, but it seems - // we need to create a new AppDomain to do this, and it doesn't work with Mono :-/ + string fname0 = Path.GetFileNameWithoutExtension( System.Reflection.Assembly.GetEntryAssembly().Location ) + ".json" ; + fname = Path.Combine( mBaseDir, fname0 ) ; + if ( ! File.Exists( fname ) ) { + fname = Path.Combine( mDataDir, fname0 ) ; + if ( ! File.Exists( fname ) ) + fname = Path.Combine( mBaseDir, "../"+fname0 ) ; + } + mAppConfig = new JsonConfig( "app config", fname ) ; + + // load the data config mDataConfig = new JsonConfig( "data config", Path.Combine( mDataDir, "config.json" ) ) ; // load the debug config @@ -55,12 +73,7 @@ public static class Program public static void showWarningMsg( string msg ) { MessageBox.Show(msg,APP_NAME,MessageBoxButtons.OK,MessageBoxIcon.Warning) ; } public static void showErrorMsg( string msg ) { MessageBox.Show(msg,APP_NAME,MessageBoxButtons.OK,MessageBoxIcon.Error) ; } - public static void logTraceMsg( string msg ) - { - // log a trace message - Trace.WriteLine( DateTime.Now.ToString("HH:mm:ss") + " | " + msg ) ; - } - + public static JsonConfig appConfig { get { return mAppConfig ; } } public static JsonConfig dataConfig { get { return mDataConfig ; } } public static JsonConfig debugConfig { get { return mDebugConfig ; } } public static string dataDir { get { return mDataDir ; } }