Search code examples
c#winformssqliteasync-awaitbing-maps

Why does only a subset of my Pins display on the Bing Map in one particular scenario?


In my app, the user can either load a text file (semi-colon delimited) which describes the characteristics of locations to be turned into [Push]Pins on a Bing Map, or load pre-existing map data (from a local database) to the same end.

In the case of loading from the database, it's working great and a Pin is displayed on the map for every corresponding record in the database, such as this:

enter image description here

But when I populate the database from the contents of the text file and then create Pins based on those values, only a subset of the pins display on the map - usually just the first two, in fact!

But then, when I load the same map from the database, all displays as it should.

I've checked the database after generating the new records from the text file, and all the records are indeed there (including the coordinates (Latitude and Longitude values). The code seems to be the same in both cases. But it's not displaying all the Pins...

The text file contents are such as this (this is what created the map shown above):

Paris, France;;Paris;France;75000
Florence, Italy;;Florence;Italy;50100
Chelsea, London, England;23 Tedworth Square;Chelsea London;England;SW3 4DY
Kaltenleutgeben, Austria;;Kaltenleutgeben;Austria;2380
Vienna, Austria;;Vienna;Austria;1010
Weggis, Switzerland;;Weggis;Switzerland;6006
Heidelberg, Germany;;Heidelberg;Germany;69115
Munich, Germany;;Munich;Germany;80331

To get down to the nitty-gritty, here is the code in the main form that loads the map data from a text file, such as the one shown immediately above:

private void toolStripMenuItemCreateMapAndLocsFromFile_Click(object sender, EventArgs e)
{
    bool pushpinsAdded = false;
    string _currentMap = string.Empty;
    RemoveCurrentPushpins();

    using (var frmCre8MapAndLocsFromFile = new mdlDlgCreateMapAndLocsFromTSVFile())
    {
        if (frmCre8MapAndLocsFromFile.ShowDialog() == DialogResult.OK)
        {
            _currentMap = mdlDlgCreateMapAndLocsFromTSVFile.CurrentMap;
            foreach (Pushpin pin in frmCre8MapAndLocsFromFile.Pins)
            {
                this.userControl11.myMap.Children.Add(pin);
                pushpinsAdded = true; // redundant after the first assignment
            }
        }
    }
    if (pushpinsAdded)
    {
        RightsizeZoomLevelForAllPushpins();
    }
    lblMapName.Text = currentMap;
    lblMapName.Visible = true;
}

Here's the pertinent code from mdlDlgCreateMapAndLocsFromTSVFile (yes, it's misnamed, because I switched from TSV to SCSV), which is invoked above:

private void btnOpenTSVFile_Click(object sender, EventArgs e)
{
    string pathToFile = string.Empty;
    string line = string.Empty;
    int counter = 0;
    openTSVFileDlg.Filter = "scsv files (*.scsv)|*.scsv|txt files (*.txt)|*.txt|All files (*.*)|*.*";
    
    if (openTSVFileDlg.ShowDialog() == DialogResult.OK)
    {
        pathToFile = openTSVFileDlg.FileName;
        fileNameOnly = Path.GetFileName(pathToFile); 
        fileNameOnly = Path.ChangeExtension(fileNameOnly, null);
        CurrentMap = fileNameOnly;
        if (SCSVFileAlreadyLoaded(fileNameOnly))
        {
            MessageBox.Show(String.Format("The file/table {0} has already been loaded/populated", fileNameOnly));
            return;
        }
        StreamReader file = new StreamReader(pathToFile);
        while ((line = file.ReadLine()) != null)
        {
            if (counter == 0)
            {
                InsertMapRecord(fileNameOnly);
            }
            counter++;
            InsertMapDetailRecord(fileNameOnly, line);
        }
    }
}

InsertMapDetailRecord() is the method that adds the pushpins to the List, and it is:

private void InsertMapDetailRecord(string fileNameOnly, string line)
{
    string keyRecordElements = string.Empty;
    string[] lineElements;
    lineElements = line.Split(';');
    string locationName = lineElements[0].Trim();
    string address = lineElements[1].Trim();
    string city = lineElements[2].Trim();
    string st8 = lineElements[3].Trim();
    string zip = lineElements[4].Trim();
    keyRecordElements = string.Format("{0} {1} {2} {3}", address, city, st8, zip).Trim();
    if (MissingValues(lineElements))
    {
        MessageBox.Show("Could not insert record - one or more key values missing");
        return;
    }

    if (InsertIntoCartographerDetail(fileNameOnly, locationName, address, city, st8, zip))
    {
        AddPushpin(keyRecordElements, locationName);
    }
    else
    {
        MessageBox.Show("insert failed");
    }
}

AddPushpin() is:

private async void AddPushpin(string _fullAddress, string _location)
{
    iContentCounter = iContentCounter + 1;
    // from https://stackoverflow.com/questions/65752688/how-can-i-retrieve-latitude-and-longitude-of-a-postal-address-using-bing-maps
    var request = new GeocodeRequest();
    request.BingMapsKey = "Gr8GooglyMoogly";
    request.Query = _fullAddress;

    var result = await request.Execute();
    if (result.StatusCode == 200)
    {
        var toolkitLocation = (result?.ResourceSets?.FirstOrDefault())
                ?.Resources?.FirstOrDefault()
                as BingMapsRESTToolkit.Location;
        var latitude = toolkitLocation.Point.Coordinates[0];
        var longitude = toolkitLocation.Point.Coordinates[1];
        var mapLocation = new Microsoft.Maps.MapControl.WPF.Location(latitude, longitude);
        this.Pin = new Pushpin() { Location = mapLocation, ToolTip = _location, Content = iContentCounter 
     };
        if (null == this.Pins)
        {
            this.Pins = new List<Pushpin>();
        }
        this.Pins.Add(Pin); // specific to this form
        UpdateDetailRecWithCoords(_location, latitude, longitude); 
        this.DialogResult = DialogResult.OK;
    }
}

Again, this does what it's supposed to do as far as populating the database, including the Detail table's Latitude and Longitude fields, but only two pushpins display on the map after focus is passed back to the main form when the modal dialog is closed.

Here is the code from the main form that invokes mdlDlgFrm_LoadExistingMap:

private void loadExistingMapToolStripMenuItem_Click(object sender, EventArgs e)
{
    bool pushpinsAdded = false;
    RemoveCurrentPushpins();
    using (var frmLoadExistingMap = new mdlDlgFrm_LoadExistingMap())
    {
        if (frmLoadExistingMap.ShowDialog() == DialogResult.OK)
        {
            foreach (Pushpin pin in frmLoadExistingMap.Pins) 
            {
                this.userControl11.myMap.Children.Add(pin);
                pushpinsAdded = true;
            }
        }
    }
    if (pushpinsAdded)
    {
        RightsizeZoomLevelForAllPushpins();
    }
    lblMapName.Text = currentMap;
    lblMapName.Visible = true;
}

And here is the code from the mdlDlgFrm_LoadExistingMap form, which :

private void btnLoadSelectedMap_Click(object sender, EventArgs e)
{
    string latitudeAsStr = string.Empty;
    string longitudeAsStr = string.Empty;
    List<MapDetails> lstMapDetails = new List<MapDetails>();
    MapDetails md;
    const int LOC_NAME = 0;
    const int ADDRESS = 1;
    const int CITY = 2;
    const int ST8 = 3;
    const int ZIP = 4;
    const int NOTES = 5;
    const int LATITUDE = 6;
    const int LONGITUDE = 7;
 
    mapName = cmbxMaps.Text.Trim();
    if (mapName == string.Empty)
    {
        MessageBox.Show("Select a map from the combo box");
        cmbxMaps.Focus();
        return;
    }
    try
    {
        Form1.currentMap = mapName; 
        string qry = "SELECT LocationName, Address1, City, StateOrSo, PostalCode, " +
                                "MapDetailNotes, Latitude, Longitude " +
                                "FROM CartographerDetail " +
                                "WHERE FKMapName = @FKMapName";
        var con = new SqliteConnection(connStr);
        con.Open();
        SqliteCommand cmd = new SqliteCommand(qry, con);
        cmd.Parameters.AddWithValue("@FKMapName", mapName);
        var reader = cmd.ExecuteReader();
        while (reader.Read())
        {
            md = new MapDetails();
            md.LocationName = reader.GetValue(LOC_NAME).ToString().Trim();
            md.Address = reader.GetValue(ADDRESS).ToString().Trim();
            md.City = reader.GetValue(CITY).ToString().Trim();
            md.StateOrSo = reader.GetValue(ST8).ToString().Trim();
            md.PostalCode = reader.GetValue(ZIP).ToString().Trim();
            md.MapDetailNotes = reader.GetValue(NOTES).ToString();
            latitudeAsStr = reader.GetValue(LATITUDE).ToString();
            if (string.IsNullOrEmpty(latitudeAsStr))
            {
                md.Latitude = 0.0;
            }
            else
            {
                md.Latitude = Convert.ToDouble(reader.GetValue(LATITUDE).ToString());
            }
            longitudeAsStr = reader.GetValue(LONGITUDE).ToString();
            if (string.IsNullOrEmpty(longitudeAsStr))
            {
                md.Longitude = 0.0;
            }
            else
            {
                md.Longitude = Convert.ToDouble(reader.GetValue(LONGITUDE).ToString());
            }
            lstMapDetails.Add(md);
        }
        AddPushpinsToListOfPushpins(lstMapDetails);
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
    }
    this.Close();
}

// AddPushpins (plural)
private void AddPushpinsToListOfPushpins(List<MapDetails> lstMapDetails)
{
    string fullAddress;

    foreach (MapDetails _md in lstMapDetails)
    {
        fullAddress = string.Format("{0} {1} {2} {3}", _md.Address, _md.City, _md.StateOrSo, _md.PostalCode).Trim();
        AddPushpinToListOfPushpins(_md.LocationName, fullAddress, _md.MapDetailNotes, _md.Latitude,                 _md.Longitude);
    }
}

// AddPushpin (singular)
private async void AddPushpinToListOfPushpins(string location, string fullAddress, string mapDetailNotes, double latitude, double longitude)
{
    iContentCounter = iContentCounter + 1;
    string toolTip = string.Empty;
    // if already have the record, including the coordinates, no need to make the REST call
    if ((latitude != 0.0) && (longitude != 0.0))
    {
        if (mapDetailNotesExist(location))
        {
            toolTip = String.Format("{0}{1}{2}{1}{3},{4}{1}{5}", location, Environment.NewLine, fullAddress, latitude,                 longitude, mapDetailNotes.Trim());
        }
        else
        {
            toolTip = String.Format("{0}{1}{2}{1}{3},{4}", location, Environment.NewLine, fullAddress, latitude,                         longitude);
        }
        var _mapLocation = new Microsoft.Maps.MapControl.WPF.Location(latitude, longitude);
        this.Pins.Add(new Pushpin()
        {
            Location = _mapLocation,
            ToolTip = toolTip,
            Content = iContentCounter
        });
    }
    else
    {
        // from https://stackoverflow.com/questions/65752688/how-can-i-retrieve-latitude-and-longitude-of-a-postal-                address-using-bing-maps
        var request = new GeocodeRequest();
        request.BingMapsKey = "Gr8GooglyMoogly";
        request.Query = fullAddress;

        var result = await request.Execute();
        if (result.StatusCode == 200)
        {
            var toolkitLocation = (result?.ResourceSets?.FirstOrDefault())
                    ?.Resources?.FirstOrDefault()
                    as BingMapsRESTToolkit.Location;
            var _latitude = toolkitLocation.Point.Coordinates[0];
            var _longitude = toolkitLocation.Point.Coordinates[1];
            var mapLocation = new Microsoft.Maps.MapControl.WPF.Location(_latitude, _longitude);
            this.Pins.Add(new Pushpin() 
            { 
                Location = mapLocation,
                ToolTip = String.Format("{0}{1}{2}{1}{3},{4}{1}{5}", location, Environment.NewLine, fullAddress, _latitude,                         _longitude, mapDetailNotes),
                Content = iContentCounter 
            });
            UpdateLocationWithCoordinates(location, _latitude, _longitude);
        }
    }
}

It seems to me the code to create the Pins and then display them in the map is the same whether I create a map from a file or if I load a map from the database. So why is the behavior different?

UPDATE

To give an even more concrete example, when creating a map via file with contents like this (excerpt of first few rows):

July 15 & 16, 1895 - Music Hall, Cleveland, Ohio;; Cleveland;Ohio;44101
July 18, 1895 - Soo Opera House or Hotel Iroquois, Sault Ste. Marie, Michigan;;Sault Ste. Marie;Michigan;49783
July 19, 1895 - Casino Room, Grand Hotel, Mackinac, Michigan;;Mackinac;Michigan;49757
July 20, 1895 - Grand Opera House, Petoskey, Michigan;;Petoskey;Michigan;49770
July 22, 1895 - First Methodist Church, Duluth, Minnesota;;Duluth;Minnesota;55802
July 23, 1895 - Hotel West & Reception and Supper, Minneapolis, Minnesota;;Minneapolis;Minnesota;55111
July 24, 1895 - People's Church, St. Paul, Minnesota;;St. Paul;Minnesota;55101
July 27, 1895 - Luncheon and Manitoba Club Supper, Winnipeg, Canada;;Winnipeg;Canada;R0G 0A1

...I get this at first (only a subset, and all of them with the same content number):

enter image description here

...but when I immediately thereafter load the map via "Load Existing" I get it all:

enter image description here

?

UPDATE 2

Thanks to "The Bing Maps Whisperer," Reza Aghaei, it is now working:

enter image description here


Solution

  • Here are the fixes that I see you need to apply:

    1. Change AddPushpin signature to async Task AddPushpin(...)
    2. Then in InsertMapDetailRecord change the call to await AddPushpin(...);
    3. Change InsertMapDetailRecord signature to async Task InsertMapDetailRecord(...)
    4. Change btnOpenTSVFile_Click signature to async void btnOpenTSVFile_Click
    5. Then in btnOpenTSVFile_Click change the call InsertMapDetailRecord to await InsertMapDetailRecord()

    I also guess you should:

    1. In AddPushpin method, remove this.DialogResult = DialogResult.OK;
    2. In btnOpenTSVFile_Click, add this.DialogResult = DialogResult.OK; right after end of while loop.

    Using async/await

    Basically when in method void M(...) you want to call a method DoSomething(...) which returns Task, change the call to await DoSomething(...);. Then if method M is an event handler, change the signature to async void M(...), otherwise if its a normal method, change the signature to async Task M(...).

    Setting DialogResult

    The reason that I think you need to change the signatures and also the line that you set dialog result: In AddPushpin, you are setting DialogResult to OK, which means after adding the first pushpin, the containing dialog (mdlDlgCreateMapAndLocsFromTSVFile) will be closed and you start looking into Pins property (which still behind the scene is getting filled, then after adding the first few pushpins which had chance to be added to Pins property, you dispose the mdlDlgCreateMapAndLocsFromTSVFile and no more pushpin will be added to the map.

    Scope of variables

    We don't see the whole code, but pay attention to the scope of the variables/fields. Just as a warning, It's not a very good idea to access class fields throughout your methods, they are not local variables and when you change those values, the change is visible to other methods as well.