Google map of shops on contact us page

2016-09-24 contact, google, map

In this guide I want to show you how to display google map on contact us page (there where contact us form is). Based on this tutorial you will be able to move whole map with all shops and all available features like shop search etc. Whole guide is based on contact Controller modification (ContactController.php) and theme template file modification that is responsible for contact us page (contact-form.tpl).

prestashop contact us page map with shops

Contact Controller modification

Firstly I want to say that to apply modifications you can use overrides mechanism (if you know how to do it i suggest to use overrides). But In this guide i will show how to do apply modifications to original ContactController.php file. To move "our stores" map to contact us page it is necessary to combine two controllers. First: controller responsible for map called StoresController and second: controller responsible for contact form called ContactController.

 

Apply necessary js and css files to handle google map and appearance of stores search form etc.

To apply these files we just need to alter setMedia() function that available in ContactController. In simple words this function controls what libraries will be attached to page <head> section. So, please open file /controllers/front/ContactController.php and search for public function setMedia();

    public function setMedia()
    {
        parent::setMedia();
        $this->addCSS(_THEME_CSS_DIR_.'contact-form.css');
        $this->addJS(_THEME_JS_DIR_.'contact-form.js');
        $this->addJS(_PS_JS_DIR_.'validate.js');
        
        parent::setMedia();
    }

change it to (i added highlighted lines):

public function setMedia()
{
    parent::setMedia();
    $this->addCSS(_THEME_CSS_DIR_.'contact-form.css');
    $this->addJS(_THEME_JS_DIR_.'contact-form.js');
    $this->addJS(_PS_JS_DIR_.'validate.js');
    parent::setMedia();
    $this->addCSS(_THEME_CSS_DIR_.'stores.css');
 
    if (!Configuration::get('PS_STORES_SIMPLIFIED')) {
        $default_country = new Country((int)Tools::getCountry());
        $this->addJS('http'.((Configuration::get('PS_SSL_ENABLED') && Configuration::get('PS_SSL_ENABLED_EVERYWHERE')) ? 's' : '').'://maps.google.com/maps/api/js?sensor=true&region='.substr($default_country->iso_code, 0, 2));
        $this->addJS(_THEME_JS_DIR_.'stores.js');
    }
}

 

Modification of InitContent function

Now it's time to assign necessary variables to contact us page like list of stores, image for store on map, longitute and latitude of map etc. In this case we need to alter InitContent() function available In the same file. Just search there for initContent() function and change its code from:

    /**
    * Assign template vars related to page content
    * @see FrontController::initContent()
    */
    public function initContent()
    {
        parent::initContent();

        $this->assignOrderList();

        $email = Tools::safeOutput(Tools::getValue('from',
        ((isset($this->context->cookie) && isset($this->context->cookie->email) && Validate::isEmail($this->context->cookie->email)) ? $this->context->cookie->email : '')));
        $this->context->smarty->assign(array(
            'errors' => $this->errors,
            'email' => $email,
            'fileupload' => Configuration::get('PS_CUSTOMER_SERVICE_FILE_UPLOAD'),
            'max_upload_size' => (int)Tools::getMaxUploadSize()
        ));

        if (($id_customer_thread = (int)Tools::getValue('id_customer_thread')) && $token = Tools::getValue('token')) {
            $customer_thread = Db::getInstance()->getRow('
				SELECT cm.*
				FROM '._DB_PREFIX_.'customer_thread cm
				WHERE cm.id_customer_thread = '.(int)$id_customer_thread.'
				AND cm.id_shop = '.(int)$this->context->shop->id.'
				AND token = \''.pSQL($token).'\'
			');

            $order = new Order((int)$customer_thread['id_order']);
            if (Validate::isLoadedObject($order)) {
                $customer_thread['reference'] = $order->getUniqReference();
            }
            $this->context->smarty->assign('customerThread', $customer_thread);
        }

        $this->context->smarty->assign(array(
            'contacts' => Contact::getContacts($this->context->language->id),
            'message' => html_entity_decode(Tools::getValue('message'))
        ));


        if (Configuration::get('PS_STORES_SIMPLIFIED')) {
            $this->assignStoresSimplified();
        } else {
            $this->assignStores();
        }

        $this->setTemplate(_PS_THEME_DIR_.'contact-form.tpl');
    }

to (i highlighted new lines of code)

    /**
    * Assign template vars related to page content
    * @see FrontController::initContent()
    */
    public function initContent()
    {
        parent::initContent();

        $this->assignOrderList();

        $email = Tools::safeOutput(Tools::getValue('from',
        ((isset($this->context->cookie) && isset($this->context->cookie->email) && Validate::isEmail($this->context->cookie->email)) ? $this->context->cookie->email : '')));
        $this->context->smarty->assign(array(
            'errors' => $this->errors,
            'email' => $email,
            'fileupload' => Configuration::get('PS_CUSTOMER_SERVICE_FILE_UPLOAD'),
            'max_upload_size' => (int)Tools::getMaxUploadSize()
        ));

        if (($id_customer_thread = (int)Tools::getValue('id_customer_thread')) && $token = Tools::getValue('token')) {
            $customer_thread = Db::getInstance()->getRow('
				SELECT cm.*
				FROM '._DB_PREFIX_.'customer_thread cm
				WHERE cm.id_customer_thread = '.(int)$id_customer_thread.'
				AND cm.id_shop = '.(int)$this->context->shop->id.'
				AND token = \''.pSQL($token).'\'
			');

            $order = new Order((int)$customer_thread['id_order']);
            if (Validate::isLoadedObject($order)) {
                $customer_thread['reference'] = $order->getUniqReference();
            }
            $this->context->smarty->assign('customerThread', $customer_thread);
        }

        $this->context->smarty->assign(array(
            'contacts' => Contact::getContacts($this->context->language->id),
            'message' => html_entity_decode(Tools::getValue('message'))
        ));


        if (Configuration::get('PS_STORES_SIMPLIFIED')) {
            $this->assignStoresSimplified();
        } else {
            $this->assignStores();
        }

        $this->context->smarty->assign(array(
            'mediumSize' => Image::getSize(ImageType::getFormatedName('medium')),
            'defaultLat' => (float)Configuration::get('PS_STORES_CENTER_LAT'),
            'defaultLong' => (float)Configuration::get('PS_STORES_CENTER_LONG'),
            'searchUrl' => $this->context->link->getPageLink('stores'),
            'logo_store' => Configuration::get('PS_STORES_ICON')
        ));

        $this->setTemplate(_PS_THEME_DIR_.'contact-form.tpl');
    }

 

Apply necessary functions that are responsible for database queries etc.

Now we need to move functions that are responsible for database queries (query to get list of shops) and some other functions that are necessary for "our stores" page and google map. In this case, in the same file as before (ContactController.php) before last closing bracket of class paste these functions:

    /**
     * Get formatted string address
     *
     * @param array $store
     *
     * @return string
     */
    protected function processStoreAddress($store)
    {
        $ignore_field = array(
            'firstname',
            'lastname'
        );

        $out_datas = array();

        $address_datas = AddressFormat::getOrderedAddressFields($store['id_country'], false, true);
        $state = (isset($store['id_state'])) ? new State($store['id_state']) : null;

        foreach ($address_datas as $data_line) {
            $data_fields = explode(' ', $data_line);
            $addr_out = array();

            $data_fields_mod = false;
            foreach ($data_fields as $field_item) {
                $field_item = trim($field_item);
                if (!in_array($field_item, $ignore_field) && !empty($store[$field_item])) {
                    $addr_out[] = ($field_item == 'city' && $state && isset($state->iso_code) && strlen($state->iso_code)) ?
                        $store[$field_item].', '.$state->iso_code : $store[$field_item];
                    $data_fields_mod = true;
                }
            }
            if ($data_fields_mod) {
                $out_datas[] = implode(' ', $addr_out);
            }
        }

        $out = implode('<br />', $out_datas);
        return $out;
    }

    /**
     * Assign template vars for simplified stores
     */
    protected function assignStoresSimplified()
    {
        $stores = Db::getInstance()->executeS('
		SELECT s.*, cl.name country, st.iso_code state
		FROM '._DB_PREFIX_.'store s
		'.Shop::addSqlAssociation('store', 's').'
		LEFT JOIN '._DB_PREFIX_.'country_lang cl ON (cl.id_country = s.id_country)
		LEFT JOIN '._DB_PREFIX_.'state st ON (st.id_state = s.id_state)
		WHERE s.active = 1 AND cl.id_lang = '.(int)$this->context->language->id);

        $addresses_formated = array();

        foreach ($stores as &$store) {
            $address = new Address();
            $address->country = Country::getNameById($this->context->language->id, $store['id_country']);
            $address->address1 = $store['address1'];
            $address->address2 = $store['address2'];
            $address->postcode = $store['postcode'];
            $address->city = $store['city'];

            $addresses_formated[$store['id_store']] = AddressFormat::getFormattedLayoutData($address);

            $store['has_picture'] = file_exists(_PS_STORE_IMG_DIR_.(int)$store['id_store'].'.jpg');
            if ($working_hours = $this->renderStoreWorkingHours($store)) {
                $store['working_hours'] = $working_hours;
            }
        }

        $this->context->smarty->assign(array(
            'simplifiedStoresDiplay' => true,
            'stores' => $stores,
            'addresses_formated' => $addresses_formated,
        ));
    }

    public function renderStoreWorkingHours($store)
    {
        global $smarty;

        $days[1] = 'Monday';
        $days[2] = 'Tuesday';
        $days[3] = 'Wednesday';
        $days[4] = 'Thursday';
        $days[5] = 'Friday';
        $days[6] = 'Saturday';
        $days[7] = 'Sunday';

        $days_datas = array();
        $hours = array();

        if ($store['hours']) {
            $hours = Tools::unSerialize($store['hours']);
            if (is_array($hours)) {
                $hours = array_filter($hours);
            }
        }

        if (!empty($hours)) {
            for ($i = 1; $i < 8; $i++) {
                if (isset($hours[(int)$i - 1])) {
                    $hours_datas = array();
                    $hours_datas['hours'] = $hours[(int)$i - 1];
                    $hours_datas['day'] = $days[$i];
                    $days_datas[] = $hours_datas;
                }
            }
            $smarty->assign('days_datas', $days_datas);
            $smarty->assign('id_country', $store['id_country']);
            return $this->context->smarty->fetch(_PS_THEME_DIR_.'store_infos.tpl');
        }
        return false;
    }

    public function getStores()
    {
        $distance_unit = Configuration::get('PS_DISTANCE_UNIT');
        if (!in_array($distance_unit, array('km', 'mi'))) {
            $distance_unit = 'km';
        }

        if (Tools::getValue('all') == 1) {
            $stores = Db::getInstance()->executeS('
			SELECT s.*, cl.name country, st.iso_code state
			FROM '._DB_PREFIX_.'store s
			'.Shop::addSqlAssociation('store', 's').'
			LEFT JOIN '._DB_PREFIX_.'country_lang cl ON (cl.id_country = s.id_country)
			LEFT JOIN '._DB_PREFIX_.'state st ON (st.id_state = s.id_state)
			WHERE s.active = 1 AND cl.id_lang = '.(int)$this->context->language->id);
        } else {
            $distance = (int)Tools::getValue('radius', 100);
            $multiplicator = ($distance_unit == 'km' ? 6371 : 3959);

            $stores = Db::getInstance()->executeS('
			SELECT s.*, cl.name country, st.iso_code state,
			('.(int)$multiplicator.'
				* acos(
					cos(radians('.(float)Tools::getValue('latitude').'))
					* cos(radians(latitude))
					* cos(radians(longitude) - radians('.(float)Tools::getValue('longitude').'))
					+ sin(radians('.(float)Tools::getValue('latitude').'))
					* sin(radians(latitude))
				)
			) distance,
			cl.id_country id_country
			FROM '._DB_PREFIX_.'store s
			'.Shop::addSqlAssociation('store', 's').'
			LEFT JOIN '._DB_PREFIX_.'country_lang cl ON (cl.id_country = s.id_country)
			LEFT JOIN '._DB_PREFIX_.'state st ON (st.id_state = s.id_state)
			WHERE s.active = 1 AND cl.id_lang = '.(int)$this->context->language->id.'
			HAVING distance < '.(int)$distance.'
			ORDER BY distance ASC
			LIMIT 0,20');
        }

        return $stores;
    }

    /**
     * Assign template vars for classical stores
     */
    protected function assignStores()
    {
        $this->context->smarty->assign('hasStoreIcon', file_exists(_PS_IMG_DIR_.Configuration::get('PS_STORES_ICON')));

        $distance_unit = Configuration::get('PS_DISTANCE_UNIT');
        if (!in_array($distance_unit, array('km', 'mi'))) {
            $distance_unit = 'km';
        }

        $this->context->smarty->assign(array(
            'distance_unit' => $distance_unit,
            'simplifiedStoresDiplay' => false,
            'stores' => $this->getStores(),
        ));
    }

 

Now it's time to apply map to theme template file

Okay, so our contactController.php supports now both contact form and map with stores. To display map in our theme - it's time to alter contact-form.tpl file. Just open it (it's available in your theme directory, for default bootstrap theme the path to file is: /themes/default-bootstrap/contact-form.tpl. At the very end of the file paste the code attached below:

<h1 class="page-heading bottom-indent">
	{l s='Our stores'}
</h1>
<div id="stores" class="contact-form-box" style="padding:20px;">
{if $simplifiedStoresDiplay}
	{if $stores|@count}
		<p class="store-title">
			<strong class="dark">
				{l s='Here you can find our store locations. Please feel free to contact us:'}
			</strong>
		</p>
	    <table class="table table-bordered">
	    	<thead>
            	<tr>
                	<th class="logo">{l s='Logo'}</th>
                    <th class="name">{l s='Store name'}</th>
                    <th class="address">{l s='Store address'}</th>
                    <th class="store-hours">{l s='Working hours'}</th>
                </tr>
            </thead>
			{foreach $stores as $store}
				<tr class="store-small">
					<td class="logo">
						{if $store.has_picture}
							<div class="store-image">
								<img src="{$img_store_dir}{$store.id_store}-medium_default.jpg" alt="{$store.name|escape:'html':'UTF-8'}" width="{$mediumSize.width}" height="{$mediumSize.height}"/>
							</div>
						{/if}
					</td>
					<td class="name">
						{$store.name|escape:'html':'UTF-8'}
					</td>
		            <td class="address">
		            {assign value=$store.id_store var="id_store"}
		            {foreach from=$addresses_formated.$id_store.ordered name=adr_loop item=pattern}
	                    {assign var=addressKey value=" "|explode:$pattern}
	                    {foreach from=$addressKey item=key name="word_loop"}
	                        <span {if isset($addresses_style[$key])} class="{$addresses_style[$key]}"{/if}>
	                            {$addresses_formated.$id_store.formated[$key|replace:',':'']|escape:'html':'UTF-8'}
	                        </span>
	                    {/foreach}
	                {/foreach}
	                	<br/>
						{if $store.phone}<br/>{l s='Phone:'} {$store.phone|escape:'html':'UTF-8'}{/if}
						{if $store.fax}<br/>{l s='Fax:'} {$store.fax|escape:'html':'UTF-8'}{/if}
						{if $store.email}<br/>{l s='Email:'} {$store.email|escape:'html':'UTF-8'}{/if}
						{if $store.note}<br/><br/>{$store.note|escape:'html':'UTF-8'|nl2br}{/if}
					</td>
		            <td class="store-hours">
						{if isset($store.working_hours)}{$store.working_hours}{/if}
		            </td>
				</tr>
			{/foreach}
	    </table>
	{/if}
{else}
	<div id="map"></div>
	<p class="store-title">
		<strong class="dark">
			{l s='Enter a location (e.g. zip/postal code, address, city or country) in order to find the nearest stores.'}
		</strong>
	</p>
    <div class="store-content">
        <div class="address-input">
            <label for="addressInput">{l s='Your location:'}</label>
            <input class="form-control grey" type="text" name="location" id="addressInput" value="{l s='Address, zip / postal code, city, state or country'}" />
        </div>
        <div class="radius-input">
            <label for="radiusSelect">{l s='Radius:'}</label>
            <select name="radius" id="radiusSelect" class="form-control">
                <option value="15">15</option>
                <option value="25">25</option>
                <option value="50">50</option>
                <option value="100">100</option>
            </select>
            <img src="{$img_ps_dir}loader.gif" class="middle" alt="" id="stores_loader" />
        </div>
        <div>
            <button name="search_locations" class="button btn btn-default button-small">
            	<span>
            		{l s='Search'}<i class="icon-chevron-right right"></i>
            	</span>
            </button>
        </div>
    </div>
    <div class="store-content-select selector3">
    	<select id="locationSelect" class="form-control">
    		<option>-</option>
    	</select>
    </div>

	<table id="stores-table" class="table table-bordered">
    	<thead>
			<tr>
                <th class="num">#</th>
                <th>{l s='Store'}</th>
                <th>{l s='Address'}</th>
                <th>{l s='Distance'}</th>
            </tr>
        </thead>
        <tbody>
        </tbody>
	</table>
{strip}
{addJsDef map=''}
{addJsDef markers=array()}
{addJsDef infoWindow=''}
{addJsDef locationSelect=''}
{addJsDef defaultLat=$defaultLat}
{addJsDef defaultLong=$defaultLong}
{addJsDef hasStoreIcon=$hasStoreIcon}
{addJsDef distance_unit=$distance_unit}
{addJsDef img_store_dir=$img_store_dir}
{addJsDef img_ps_dir=$img_ps_dir}
{addJsDef searchUrl=$searchUrl}
{addJsDef logo_store=$logo_store}
{addJsDefL name=translation_1}{l s='No stores were found. Please try selecting a wider radius.' js=1}{/addJsDefL}
{addJsDefL name=translation_2}{l s='store found -- see details:' js=1}{/addJsDefL}
{addJsDefL name=translation_3}{l s='stores found -- view all results:' js=1}{/addJsDefL}
{addJsDefL name=translation_4}{l s='Phone:' js=1}{/addJsDefL}
{addJsDefL name=translation_5}{l s='Get directions' js=1}{/addJsDefL}
{addJsDefL name=translation_6}{l s='Not found' js=1}{/addJsDefL}
{/strip}
{/if}
</div>

 

And that's all! You can now check the contact us page to see the effect. If you dont see the map - clear your shop cache and recompile the theme (there is just a chance that shop remember old template files so clear step is necessary).

author milos myszczuk
Article by Milosz Myszczuk PrestaShop expert, official PrestaShop community moderator. PHP developer, specialist in relative and spatial databases management, GIS Analyst, CEO & founder of VEKIA interactive agency. Read more about VEKIA company
If you like my articles and want much more valuable tips, feel free to send me donation
1.4 version 1.4.11 1.6 404 addon admin advertise ahref ajax alpha animation api app application authentication back office backup badge banner basics block bootstrap button cache carrier cart catalog category certificate changelog chat class clear client clip cms code colors columns comments configuration contact container content controller cookie counter country coupon css csv currency customer dashboard database debug default delete delivery desktop developer device disable discount displayNav displayTop download dynamic editor effect empty encrypt engine error exchange exclude export facebook faceshop fade fancoupon fancybox fanpage fatal feature feed field file fix fixed font footer free friendly url front ftp full gallery generate gift global godaddy google google+ gray grid groupon header help hide highlight homefeatured homepage hook hosting hover howto htaccess html html5 ID image import include input instagram installation integration iPhone issue javascript jquery kgb knowhow languages law left likebox link list livingsocial loading log login logo loyality mail mailing maintenance manufacturer marketing marquee mcrypt menu meta mobile modification module movie moving multilanguage multiupload must have mysql news newsletter notification number open graph order override page password performance PHP phpmyadmin picture pinterest plugin popup post prestashop prestashop 1.0 prestashop 1.1 prestashop 1.2 prestashop 1.3 prestashop 1.4 prestashop 1.5 price rules problem product profile promotion proslider purifier quantity query quick tip random rates register reinsurance release reporting reset responsive restore results ribbon rich text right sales search security seo service shadow share shipping shop shopmania slider smarty social networks SQL SSL statistics stock store style subcategory superuser support switcher tab tablet tag tax template text theme tinyMCE tips and tricks tpl tracking translations tree trends trigger tumblr tutorial twitter update upgrade upload variables video visits voucher vulnerability web2print wide widget width window wishlist wysiwyg youtube zip zopim