Portal Engine Questions on portal engine and web parts.
Version 6.x > Portal Engine > Google Maps with Zip Code Radius Search View modes: 
User avatar
Member
Member
bprigge-lojack - 1/14/2013 12:30:16 PM
   
Google Maps with Zip Code Radius Search
Has anyone implemented the Google Maps web part with dynamically populated data using zip code and radius search? How did you populate the data and what did you use to search?

User avatar
Kentico Legend
Kentico Legend
Brenden Kehren - 1/15/2013 11:11:14 PM
   
RE:Google Maps with Zip Code Radius Search
I've not used zip code and radius but have used a simple textbook with city and/or state and/or zip code and implemented Google Maps web part. The web part needs latitude and longitude to function properly (unless you create your own web part). My data is stored in a custom table as a document type was overkill since the data was managed from an outside source and updated daily.

What I did was created an ascx control and placed it in a custom filter web part, then assigned that filter web part to a basic data source then applied the data source to a Basic Google Maps web part.

If you'd like some code samples I can provide them.

User avatar
Member
Member
bprigge-lojack - 1/16/2013 11:36:53 AM
   
RE:Google Maps with Zip Code Radius Search
Code Samples would be great!

User avatar
Kentico Legend
Kentico Legend
Brenden Kehren - 1/16/2013 10:11:55 PM
   
RE:Google Maps with Zip Code Radius Search
Apoligize ahead of time for the length but I hope it's worth it.

Short version:
I created a page called locations. Added a Custom Table Data Source webpart as all of my locations are stored in a custom table. I named the ID as LocationsDataSource and assigned the Data Filter id as LocationsFilter. I then added a Basic Repeater webpart and assigned the Data source name as LocationsDataSource and assigned transformations as needed. This will display the results in a nice list. I then added a Filter webpart and named it LocationsFilter and assigned the location of the .ascx control I created (talk about later). Then I added a BasicGoogleMaps webpart and assigned the DataSource name as LocationsDataSource and set the map properties as needed.

Details:
The filter was key to the whole operation. I knew what was required would be a challenge and it proved to be but now that I've done it, way easy. Her e is the locations filter code.
<asp:Panel ID="pnlLocationSearch" runat="server" DefaultButton="btnGo" CssClass="location-search-filter">
<h3>Refine Your Search</h3>
<div class="search-result-text">Select your parameters, enter a city, city and state OR ZIP code and click GO.</div><br />
<cms:LocalizedCheckBoxList ID="cblItems" runat="server" DataTextField="FriendlyName" DataValueField="FieldName" RepeatColumns="3" RepeatDirection="Horizontal"/><br />
<cms:LocalizedRadioButtonList ID="rblDistance" runat="server" DataTextField="FriendlyName" DataValueField="Distance" RepeatLayout="UnorderedList"/><br />
<div><asp:TextBox ID="txtSearch" runat="server" Width="50%" />
<cms:CMSButton ID="btnGo" runat="server" Text="Go" EnableViewState="false" SkinID="SearchButton" CssClass="input-button" OnClick="btnGo_Click" /></div><br />
<asp:HiddenField ID="UserLatitude" runat="server" />
<asp:HiddenField ID="UserLongitude" runat="server" />
<asp:HiddenField ID="CanDetectGeo" runat="server" />
</asp:Panel>

Pretty simple UI, has a check box list that displays a list of attributes that could be available at the locations, a radio button list of distances to search from (could be any variation of 10 miles, 50 miles, 1000 miles), and a textbox to enter search criteria in (city, city and state, city or state, zip code, etc).
The code behind of the filter:

using System;
using System.Data;
using System.Collections.Generic;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

using CMS.GlobalHelper;
using CMS.Controls;
using CMS.SettingsProvider;
using CMS.DataEngine;
using CMS.PortalControls;
using CMS.CMSHelper;

public partial class KT_Controls_LocationFilter : CMSAbstractQueryFilterControl
{
private double mLatitude = 0.0;
private double mLongitude = 0.0;
private bool mCanDetectGeo = false;

protected override void OnInit(EventArgs e)
{
SetupControl();
base.OnInit(e);
}

protected override void OnPreRender(EventArgs e)
{
mLatitude = ValidationHelper.GetDouble(UserLatitude.Value, 0.0);
mLongitude = ValidationHelper.GetDouble(UserLongitude.Value, 0.0);
mCanDetectGeo = ValidationHelper.GetBoolean(CanDetectGeo.Value, false);

if (RequestHelper.IsPostBack())
{
if (mCanDetectGeo)
{
// nothing
}
else
{
SetFilter();
}
}
else
{
// on initial page load set empty filter
SetEmptyFilter();
}
base.OnPreRender(e);
}

/// <summary>
/// Do this to not display anything until the GO button is clicked?
/// </summary>
private void SetEmptyFilter()
{
this.WhereCondition = "1=2";
this.SelectTopN = 0;
this.RaiseOnFilterChanged();
}

private void SetupControl()
{
if (this.StopProcessing)
{
//this.Visible = false;
}
else
{
CMS.GlobalHelper.ScriptHelper.RegisterScriptFile(this.Page, "~/KT/Controls/LocationFilter_Files/autolocation.js"); // for map and html5 use

if (!RequestHelper.IsPostBack())
{
GetSearchableAttributes();
GetDistances();
}
}
}

/// <summary>
/// Populate the checkbox list with filter items from custom table
/// </summary>
protected void GetSearchableAttributes()
{
QueryDataParameters parameters = new QueryDataParameters();
GeneralConnection cn = ConnectionHelper.GetConnection();
DataSet ds = cn.ExecuteQuery("KT.SearchableFields.selectall", parameters, "KTComVisible = 1", "FriendlyName");

if (!DataHelper.DataSourceIsEmpty(ds))
{
this.cblItems.DataSource = ds;
this.cblItems.DataBind();
}
}

/// <summary>
/// Gets the distance values from custom table
/// and set default value.
/// </summary>
protected void GetDistances()
{
QueryDataParameters parameters = new QueryDataParameters();
GeneralConnection cn = ConnectionHelper.GetConnection();
DataSet ds = cn.ExecuteQuery("KT.LocationDistance.selectall", parameters, null, "ItemOrder");

if (!DataHelper.DataSourceIsEmpty(ds))
{
this.rblDistance.DataSource = ds;
this.rblDistance.DataBind();

// preselect the radio button
foreach (DataRow dr in ds.Tables[0].Rows)
{
if (ValidationHelper.GetBoolean(dr["IsDefault"], false))
{
rblDistance.Items.FindByValue(ValidationHelper.GetString(dr["Distance"], "20")).Selected = true;
}
}
}
}

/// <summary>
/// Creates the WHERE condition based on location lat/long and selections in the checkboxlist
/// </summary>
protected void SetFilter()
{
KT.GeoLocation gl = new KT.GeoLocation();
gl.SearchText = txtSearch.Text.Trim();
gl.Geocode();
string fromLocation = gl.Latitude.ToString() + "," + gl.Longitude.ToString();
// set session value to be able to set direction link in other transformations
SessionHelper.SetValue("FromLocation", fromLocation);

// set hidden fields for javascript references.
UserLatitude.Value = gl.Latitude.ToString();
UserLongitude.Value = gl.Longitude.ToString();
string where = "GEOGRAPHY::Point(Latitude, Longitude, 4326).STDistance('POINT(" + gl.Longitude.ToString() + " " + gl.Latitude.ToString() + ")') <= (" + rblDistance.SelectedValue + "*1609.344)";

// create fiter for selected items
foreach (ListItem item in cblItems.Items)
{
if (item.Selected)
{
where += " AND " + item.Value + " = 1";
}
}

this.SelectedColumns = "*, GEOGRAPHY::Point(Latitude, Longitude, 4326).STDistance('POINT(" + gl.Longitude.ToString() + " " + gl.Latitude.ToString() + ")')/1609.344 AS ProxDistance";
// creates a string for a from location column to get directions
this.SelectedColumns += ", '" + fromLocation + "' AS FromLocation";
// creates a map marker URL and generates a letter vs bullet point
string letter = "CHAR(64+ROW_NUMBER() OVER (ORDER BY GEOGRAPHY::Point(Latitude, Longitude, 4326).STDistance('POINT(" + gl.Longitude.ToString() + " " + gl.Latitude.ToString() + ")')/1609.344))";
string url = "'http://chart.apis.google.com/chart?cht=d&chdp=mapsapi&chl=pin%27i%5c%27%5b' + " + letter + " + '%27-2%27f%5chv%27a%5c%5dh%5c%5do%5cE1373E%27fC%5cFFFFFF%27tC%5cEFE5CD%27eC%5cLauto%27f%5c&ext=.png'";
this.SelectedColumns += ", " + url + " AS MapMarker";
this.SelectedColumns += ", " + letter + " AS MapIndex";
this.WhereCondition = where;
this.OrderBy = "ProxDistance ASC";
this.SelectTopN = 25;
this.RaiseOnFilterChanged();
}
}

You'll see there is a class called KT.GeoLocation(), that is where we get an actual latitude and longitude based on what a user enters in the textbox. We don't use a google or any other API for that as we already have a database of city, state, zip code and geocode data so didn't see it worthy to sepend the money for the service. I won't be including that code as it is really specific to the database I'm using and some business rules. Really if you make less than 2500 requests per month, you could use google's api for free or get the paid service or find other means. So however you geocode what the user enters is totally up to you but it is needed in order for the code in the filter to work properly and query the custom table database in Kentico.

I should step back and state the query I generate in the filter for the SelectColumns and Where Clause will only work in SQL Server 2008 and newer as it is using GEOGRAPHY, SQL 2005 and earlier do not have this ability. I should also state the locations I am querying have accurate geocode data in them as well.

Really this is it! Kentico and the other webparts will handle the rest of the heavy lifting as long as you have the webparts configured properly. So what ends up happening is when the page loads there will be no results and no pins on the map and a clean filter. Someone types in a city and state combination and clicks GO and the filter runs off and attempts to geocode the city and state they entered and sets the SelectedColumns and WhereClause for the LocationsDataSource which queries the Locations custom table. Once the LocationsDataSource is finished it binds to the BasicRepeater and the BasicGoogleMap and displays the results in a list on on the map with all the markers.

I should also note in the filter there is a string URL that I use for the MapMarker field (which is set as the Icon URL in the map webpart), this is a url from google that you can create a custom looking pin on the map. For instance we didn't want the plain old red marker with the black dot so we changed the fill and outline as well as change the dot to a letter (vs. an index number).

We had a need for a custom styled info window as well, so I used the Info Bubble that from Google. I won't go into details on how I got that working but here is some good examples of what I used to get the info bubble working.

Hope this helps or at least gets you pointed in the right direction.

User avatar
Certified Developer 8
Certified Developer 8
moizkool-gmail - 3/25/2013 10:17:59 PM
   
RE:Google Maps with Zip Code Radius Search
Super FroggEye, a good example.

However, is there possible for setting up multiple markers using sets of latitude and longitudes?

User avatar
Kentico Legend
Kentico Legend
Brenden Kehren - 3/26/2013 1:33:42 PM
   
RE:Google Maps with Zip Code Radius Search
@moizkool, yes. The filter example I give is used in conjunction with a custom table datasource which is tied to a basic repeater and the basic google maps webpart. So I use the custom filter I created to filter the datasource, which in turn binds to the repeater and the google map. So if the filter filters the datasource with 10 results, the repeater will have 10 records and the map will have 10 markers on it. See the below image (sorry removed some info as we aren't quite live yet).User image

User avatar
Kentico Legend
Kentico Legend
Brenden Kehren - 1/16/2013 10:14:04 PM
   
RE:Google Maps with Zip Code Radius Search
I should have noted, I am using Kentico v7.0.9 for all of this. Based on my experience with Kentico, it all should work with v6 but there might be a few code changes you might need to make.