ความต้องการผลิตภัณฑ์และบริการบางอย่างจะแตกต่างกันไปโดยขึ้นอยู่กับสภาพอากาศ ตัวอย่างเช่น ผู้ใช้มีแนวโน้มที่จะค้นหาข้อมูลเกี่ยวกับสวนสนุกในวันที่อากาศร้อนและมีแดดจัดสูงกว่าเวลาที่อากาศหนาวและฝนตกมาก บริษัทสวนสนุกแห่งหนึ่งอาจต้องการเพิ่มราคาเสนอเมื่อสภาพอากาศดี แต่การทำเช่นนั้นในทุกๆ วันต้องอาศัยการทำงานด้วยตนเองอย่างมาก แต่เมื่อใช้สคริปต์ Google Ads คุณจะสามารถดึงข้อมูลสภาพอากาศโดยใช้โปรแกรม และปรับราคาเสนอได้ในไม่กี่นาที
สคริปต์นี้ใช้สเปรดชีตของ Google ในการจัดเก็บรายการแคมเปญและสถานที่ที่เกี่ยวข้อง การเรียก OpenWeatherMap API สำหรับสถานที่ตั้งแต่ละแห่งและสภาพอากาศจะคำนวณโดยใช้กฎพื้นฐานบางอย่าง หากกฎประเมินค่าได้เป็น "จริง" ระบบจะใช้ตัวคูณราคาเสนอตามสถานที่ตั้งที่เกี่ยวข้องกับการกำหนดสถานที่เป้าหมายสำหรับแคมเปญ
วิธีการทำงาน
สคริปต์ทำงานโดยการอ่านข้อมูลนอกสเปรดชีต สเปรดชีตประกอบด้วย แผ่นงาน 3 แผ่น ดังนี้
1. ข้อมูลแคมเปญ
ชุดกฎจะกำหนดตัวปรับราคาเสนอที่จะใช้กับแคมเปญเมื่อตรงกับสภาพอากาศ คอลัมน์ที่จำเป็นมีดังนี้
- ชื่อแคมเปญ: ชื่อของแคมเปญที่จะแก้ไข
- ตำแหน่งสภาพอากาศ: ตำแหน่งที่จะตรวจสอบสภาพอากาศ
- สภาพอากาศ: สภาพอากาศที่จะใช้กฎนี้
- ตัวปรับราคาเสนอ: ตัวปรับราคาเสนอตามสถานที่ตั้งที่จะนำไปใช้หากเป็นไปตามสภาพอากาศ
- ใช้ตัวปรับแต่งกับ: เลือกว่าจะใช้ตัวปรับราคาเสนอกับเป้าหมายตามภูมิศาสตร์ของแคมเปญที่ตรงกับสภาพอากาศที่สถานที่เท่านั้น หรือใช้กับเป้าหมายทางภูมิศาสตร์ทั้งหมดของแคมเปญ
- เปิดใช้: ระบุ
Yes
เพื่อเปิดใช้กฎและNo
เพื่อปิดใช้
ตัวอย่าง
ตัวอย่างต่อไปนี้มี 3 แคมเปญ
แคมเปญทดสอบ 1 แสดงสถานการณ์การใช้งานทั่วไป แคมเปญกำหนดเป้าหมายไปที่ บอสตัน รัฐแมสซาชูเซตส์ และมีกฎ 2 ข้อคือ
- ใช้ตัวปรับราคาเสนอเป็น
1.3
หากสภาพอากาศในบอสตัน รัฐแมสซาชูเซตส์ คือSunny
- ใช้ตัวปรับราคาเสนอเป็น
0.8
หากสภาพอากาศในบอสตัน รัฐแมสซาชูเซตส์ คือRainy
แคมเปญทดสอบ 2 มีกฎการเสนอราคาเหมือนกับแคมเปญทดสอบ 1 แต่กำหนดเป้าหมายที่คอนเนตทิคัต
แคมเปญทดสอบ 3 ยังใช้กฎการเสนอราคาเดียวกันแต่กำหนดเป้าหมายไปยังฟลอริดาด้วย เนื่องจากกฎสภาพอากาศสำหรับฟลอริดาจับคู่กับทั้งรัฐ ซึ่งไม่ใช่สถานที่ที่แคมเปญกำหนดเป้าหมายอย่างชัดแจ้ง การตั้งค่า "ใช้ตัวปรับแต่งใน" จึงตั้งค่าเป็น All Geo Targets
เพื่อให้เมืองที่เป็นเป้าหมายของแคมเปญได้รับผลกระทบ
2. ข้อมูลสภาพอากาศ
ชีตนี้ระบุสภาพอากาศที่ใช้ในเอกสารข้อมูลแคมเปญ ต้องมีคอลัมน์ต่อไปนี้
- ชื่อเงื่อนไข: ชื่อสภาพอากาศ (เช่น
Sunny
) - อุณหภูมิ: อุณหภูมิในหน่วยฟาเรนไฮต์
- ฝน/ลูกเห็บ/หิมะ: ฝนในช่วง 3 ชั่วโมงที่ผ่านมา (หน่วยเป็นมิลลิเมตร)
- ลม: ความเร็วลมในหน่วยไมล์ต่อชั่วโมง
แผ่นงานที่แสดงด้านบนระบุสภาพอากาศ 2 แบบ ดังนี้
Sunny
: อุณหภูมิอยู่ระหว่าง 65 ถึง 80 องศาฟาเรนไฮต์ ปริมาณน้ำฝน ในช่วง 3 ชั่วโมงที่ผ่านมา จะอยู่ที่ต่ำกว่า 1 มม. และมีความเร็วลมต่ำกว่า 5 ไมล์/ชม.Rainy
: ปริมาณฝนสูงกว่า 0 มม. ในช่วง 3 ชั่วโมงที่ผ่านมา และมีความเร็วลมน้อยกว่า 10 ไมล์/ชม.
สภาพอากาศ
เมื่อระบุสภาพอากาศ ให้ระบุค่าดังต่อไปนี้
below x
: ค่าที่ระบุคือbelow x
(เช่นbelow 10
)above x
: ค่าที่ระบุคือabove x
(เช่นabove 70
)x to y
: ค่าที่ระบุอยู่ระหว่างx
ถึงy
เท่านั้น (เช่น65 to 80
)
หากปล่อยเซลล์ว่างไว้ ระบบจะไม่พิจารณาพารามิเตอร์นั้นในการคำนวณ ในตัวอย่างของเรา เนื่องจากสภาพอากาศของ Rainy
มีคอลัมน์อุณหภูมิที่ว่างเปล่า ระบบจึงไม่พิจารณาอุณหภูมิเมื่อคำนวณสภาพอากาศนี้
สภาพอากาศจะมีการ "และ" รวมกันเมื่อคำนวณสภาพอากาศ ในตัวอย่างนี้ สภาพอากาศของ Sunny
จะได้รับการประเมินดังนี้
const isSunny = (temperature >= 65 && temperature <= 80) && (precipitation < 1) && (wind < 5);
3. ข้อมูลตำแหน่งสภาพอากาศ
ชีตนี้กำหนดตำแหน่งสภาพอากาศที่ใช้ในเอกสารข้อมูลแคมเปญ และประกอบด้วย 2 คอลัมน์ ดังนี้
- ตำแหน่งสภาพอากาศ: ชื่อตำแหน่งสภาพอากาศที่ OpenWeatherMap API เข้าใจ
- โค้ดเป้าหมายทางภูมิศาสตร์: นี่คือโค้ดเป้าหมายทางภูมิศาสตร์ตามที่ Google Ads เข้าใจ
สคริปต์นี้จะช่วยคุณระบุรหัสเป้าหมายทางภูมิศาสตร์หลายรหัสสำหรับสถานที่สภาพอากาศเดียวได้ เนื่องจากสถานที่สภาพอากาศไม่ได้ละเอียดเท่ากับตัวเลือกการกำหนดเป้าหมายที่มีอยู่ใน Google Ads เสมอไป การแมปตำแหน่งสภาพอากาศเดียวกับตำแหน่งทางภูมิศาสตร์หลายแห่งสามารถทำได้โดยให้มีแถวหลายแถวที่มีตำแหน่งสภาพอากาศเดียวกัน แต่มีรหัสภูมิศาสตร์ต่างกันในแต่ละแถว
ในตัวอย่างนี้มีการกำหนดตำแหน่งสภาพอากาศไว้ 3 ตำแหน่ง ดังนี้
Boston, MA
: รหัสภูมิศาสตร์10108127
Connecticut
: รหัสภูมิศาสตร์1014778
,1014743
และ1014843
สอดคล้องกับเมือง 3 เมืองในคอนเนตทิคัตFlorida
: รหัสภูมิศาสตร์21142
การกำหนดเป้าหมายที่ใกล้เคียง
กฎแคมเปญที่ใช้ Matching Geo Targets
จะใช้กับสถานที่ที่กำหนดเป้าหมาย สถานที่ใกล้เคียงเป้าหมาย หรือทั้ง 2 อย่างได้โดยใช้แฟล็ก TARGETING
การกำหนดสถานที่เป้าหมายจะจับคู่รหัสภูมิศาสตร์กับรหัสสถานที่ตั้ง
การกำหนดเป้าหมายบริเวณใกล้เคียงจะยืนยันว่าพิกัดละติจูดและลองจิจูดที่ระบุอยู่ในรัศมีบริเวณใกล้เคียงโดยใช้สูตร Haversine
ตรรกะสคริปต์
สคริปต์จะเริ่มด้วยการอ่านกฎจากทั้ง 3 ชีต จากนั้นพยายามเรียกใช้กฎแต่ละข้อจากชีตแคมเปญตามลำดับ
สำหรับกฎที่ดำเนินการแต่ละกฎ สคริปต์จะตรวจสอบว่าแคมเปญกำหนดเป้าหมายไปยังสถานที่ที่ระบุหรือไม่ แต่หากมี สคริปต์จะดึงข้อมูลตัวปรับราคาเสนอปัจจุบัน
จากนั้น ระบบจะดึงข้อมูลสภาพอากาศสำหรับตำแหน่งนั้นด้วยการเรียก OpenWeatherMap API จากนั้นระบบจะประเมินกฎสภาพอากาศเพื่อดูว่าสภาพอากาศในสถานที่ตรงกับสิ่งที่ระบุไว้ในกฎหรือไม่ หากใช่ และตัวปรับราคาเสนอใหม่จะต่างจากตัวปรับราคาเสนอปัจจุบัน สคริปต์จะแก้ไขตัวปรับราคาเสนอสำหรับสถานที่ตั้งนั้น
ระบบจะไม่ทำการเปลี่ยนแปลงหากสภาพอากาศไม่ตรงกัน หากค่าของตัวปรับราคาเสนอเหมือนกัน หรือใช้ตัวปรับเปลี่ยนกับของกฎคือ Matching Geo
Targets
แต่แคมเปญไม่ได้กำหนดเป้าหมายสถานที่ที่จับคู่กับกฎ
ตั้งค่า
- ลงทะเบียนรับคีย์ API ที่ openweathermap.org
- ทำสำเนาสเปรดชีตเทมเพลต แล้วแก้ไขกฎของแคมเปญและสภาพอากาศ
- สร้างสคริปต์ใหม่โดยมีซอร์สโค้ดด้านล่าง
- อัปเดตตัวแปร
OPEN_WEATHER_MAP_API_KEY
,SPREADSHEET_URL
และTARGETING
ในสคริปต์ - ตั้งเวลาให้ทำงานตามที่ต้องการ
ซอร์สโค้ด
// Copyright 2015, Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @name Bid By Weather
*
* @overview The Bid By Weather script adjusts campaign bids by weather
* conditions of their associated locations. See
* https://developers.google.com/google-ads/scripts/docs/solutions/weather-based-campaign-management#bid-by-weather
* for more details.
*
* @author Google Ads Scripts Team [adwords-scripts@googlegroups.com]
*
* @version 2.0
*
* @changelog
* - version 2.0
* - Updated to use new Google Ads Scripts features.
* - version 1.2.2
* - Add support for video and shopping campaigns.
* - version 1.2.1
* - Added validation for external spreadsheet setup.
* - version 1.2
* - Added proximity based targeting. Targeting flag allows location
* targeting, proximity targeting or both.
* - version 1.1
* - Added flag allowing bid adjustments on all locations targeted by
* a campaign rather than only those that match the campaign rule
* - version 1.0
* - Released initial version.
*/
// Register for an API key at http://openweathermap.org/appid
// and enter the key below.
const OPEN_WEATHER_MAP_API_KEY = 'INSERT_OPEN_WEATHER_MAP_API_KEY_HERE';
// Create a copy of https://goo.gl/A59Uuc and enter the URL below.
const SPREADSHEET_URL = 'INSERT_SPREADSHEET_URL_HERE';
// A cache to store the weather for locations already lookedup earlier.
const WEATHER_LOOKUP_CACHE = {};
// Flag to pick which kind of targeting "LOCATION", "PROXIMITY", or "ALL".
const TARGETING = 'ALL';
/**
* According to the list of campaigns and their associated locations, the script
* makes a call to the OpenWeatherMap API for each location.
* Based on the weather conditions, the bids are adjusted.
*/
function main() {
validateApiKey();
// Load data from spreadsheet.
const spreadsheet = validateAndGetSpreadsheet(SPREADSHEET_URL);
const campaignRuleData = getSheetData(spreadsheet, 1);
const weatherConditionData = getSheetData(spreadsheet, 2);
const geoMappingData = getSheetData(spreadsheet, 3);
// Convert the data into dictionaries for convenient usage.
const campaignMapping = buildCampaignRulesMapping(campaignRuleData);
const weatherConditionMapping =
buildWeatherConditionMapping(weatherConditionData);
const locationMapping = buildLocationMapping(geoMappingData);
// Apply the rules.
for (const campaignName in campaignMapping) {
applyRulesForCampaign(campaignName, campaignMapping[campaignName],
locationMapping, weatherConditionMapping);
}
}
/**
* Retrieves the data for a worksheet.
*
* @param {Object} spreadsheet The spreadsheet.
* @param {number} sheetIndex The sheet index.
* @return {Array} The data as a two dimensional array.
*/
function getSheetData(spreadsheet, sheetIndex) {
const sheet = spreadsheet.getSheets()[sheetIndex];
const range =
sheet.getRange(2, 1, sheet.getLastRow() - 1, sheet.getLastColumn());
return range.getValues();
}
/**
* Builds a mapping between the list of campaigns and the rules
* being applied to them.
*
* @param {Array} campaignRulesData The campaign rules data, from the
* spreadsheet.
* @return {!Object.<string, Array.<Object>> } A map, with key as campaign name,
* and value as an array of rules that apply to this campaign.
*/
function buildCampaignRulesMapping(campaignRulesData) {
const campaignMapping = {};
for (const rules of campaignRulesData) {
// Skip rule if not enabled.
if (rules[5].toLowerCase() == 'yes') {
const campaignName = rules[0];
const campaignRules = campaignMapping[campaignName] || [];
campaignRules.push({
'name': campaignName,
// location for which this rule applies.
'location': rules[1],
// the weather condition (e.g. Sunny).
'condition': rules[2],
// bid modifier to be applied.
'bidModifier': rules[3],
// whether bid adjustments should by applied only to geo codes
// matching the location of the rule or to all geo codes that
// the campaign targets.
'targetedOnly': rules[4].toLowerCase() ==
'matching geo targets'
});
campaignMapping[campaignName] = campaignRules;
}
}
Logger.log('Campaign Mapping: %s', campaignMapping);
return campaignMapping;
}
/**
* Builds a mapping between a weather condition name (e.g. Sunny) and the rules
* that correspond to that weather condition.
*
* @param {Array} weatherConditionData The weather condition data from the
* spreadsheet.
* @return {!Object.<string, Array.<Object>>} A map, with key as a weather
* condition name, and value as the set of rules corresponding to that
* weather condition.
*/
function buildWeatherConditionMapping(weatherConditionData) {
const weatherConditionMapping = {};
for (const weatherCondition of weatherConditionData) {
const weatherConditionName = weatherCondition[0];
weatherConditionMapping[weatherConditionName] = {
// Condition name (e.g. Sunny)
'condition': weatherConditionName,
// Temperature (e.g. 50 to 70)
'temperature': weatherCondition[1],
// Precipitation (e.g. below 70)
'precipitation': weatherCondition[2],
// Wind speed (e.g. above 5)
'wind': weatherCondition[3]
};
}
Logger.log('Weather condition mapping: %s', weatherConditionMapping);
return weatherConditionMapping;
}
/**
* Builds a mapping between a location name (as understood by OpenWeatherMap
* API) and a list of geo codes as identified by Google Ads scripts.
*
* @param {Array} geoTargetData The geo target data from the spreadsheet.
* @return {!Object.<string, Array.<Object>>} A map, with key as a locaton name,
* and value as an array of geo codes that correspond to that location
* name.
*/
function buildLocationMapping(geoTargetData) {
const locationMapping = {};
for (const geoTarget of geoTargetData) {
const locationName = geoTarget[0];
const locationDetails = locationMapping[locationName] || {
'geoCodes': [] // List of geo codes understood by Google Ads scripts.
};
locationDetails.geoCodes.push(geoTarget[1]);
locationMapping[locationName] = locationDetails;
}
Logger.log('Location Mapping: %s', locationMapping);
return locationMapping;
}
/**
* Applies rules to a campaign.
*
* @param {string} campaignName The name of the campaign.
* @param {Object} campaignRules The details of the campaign. See
* buildCampaignMapping for details.
* @param {Object} locationMapping Mapping between a location name (as
* understood by OpenWeatherMap API) and a list of geo codes as
* identified by Google Ads scripts. See buildLocationMapping for details.
* @param {Object} weatherConditionMapping Mapping between a weather condition
* name (e.g. Sunny) and the rules that correspond to that weather
* condition. See buildWeatherConditionMapping for details.
*/
function applyRulesForCampaign(campaignName, campaignRules, locationMapping,
weatherConditionMapping) {
for (const rules of campaignRules) {
let bidModifier = 1;
const campaignRule = rules;
// Get the weather for the required location.
const locationDetails = locationMapping[campaignRule.location];
const weather = getWeather(campaignRule.location);
Logger.log('Weather for %s: %s', locationDetails, weather);
// Get the weather rules to be checked.
const weatherConditionName = campaignRule.condition;
const weatherConditionRules = weatherConditionMapping[weatherConditionName];
// Evaluate the weather rules.
if (evaluateWeatherRules(weatherConditionRules, weather)) {
Logger.log('Matching Rule found: Campaign Name = %s, location = %s, ' +
'weatherName = %s,weatherRules = %s, noticed weather = %s.',
campaignRule.name, campaignRule.location,
weatherConditionName, weatherConditionRules, weather);
bidModifier = campaignRule.bidModifier;
if (TARGETING == 'LOCATION' || TARGETING == 'ALL') {
// Get the geo codes that should have their bids adjusted.
const geoCodes = campaignRule.targetedOnly ?
locationDetails.geoCodes : null;
adjustBids(campaignName, geoCodes, bidModifier);
}
if (TARGETING == 'PROXIMITY' || TARGETING == 'ALL') {
const location = campaignRule.targetedOnly ? campaignRule.location : null;
adjustProximityBids(campaignName, location, bidModifier);
}
}
}
return;
}
/**
* Converts a temperature value from kelvin to fahrenheit.
*
* @param {number} kelvin The temperature in Kelvin scale.
* @return {number} The temperature in Fahrenheit scale.
*/
function toFahrenheit(kelvin) {
return (kelvin - 273.15) * 1.8 + 32;
}
/**
* Evaluates the weather rules.
*
* @param {Object} weatherRules The weather rules to be evaluated.
* @param {Object.<string, string>} weather The actual weather.
* @return {boolean} True if the rule matches current weather conditions,
* False otherwise.
*/
function evaluateWeatherRules(weatherRules, weather) {
// See https://openweathermap.org/weather-data
// for values returned by OpenWeatherMap API.
let precipitation = 0;
if (weather.rain && weather.rain['3h']) {
precipitation = weather.rain['3h'];
}
const temperature = toFahrenheit(weather.main.temp);
const windspeed = weather.wind.speed;
return evaluateMatchRules(weatherRules.temperature, temperature) &&
evaluateMatchRules(weatherRules.precipitation, precipitation) &&
evaluateMatchRules(weatherRules.wind, windspeed);
}
/**
* Evaluates a condition for a value against a set of known evaluation rules.
*
* @param {string} condition The condition to be checked.
* @param {Object} value The value to be checked.
* @return {boolean} True if an evaluation rule matches, false otherwise.
*/
function evaluateMatchRules(condition, value) {
// No condition to evaluate, rule passes.
if (condition == '') {
return true;
}
const rules = [matchesBelow, matchesAbove, matchesRange];
for (const rule of rules) {
if (rule(condition, value)) {
return true;
}
}
return false;
}
/**
* Evaluates whether a value is below a threshold value.
*
* @param {string} condition The condition to be checked. (e.g. below 50).
* @param {number} value The value to be checked.
* @return {boolean} True if the value is less than what is specified in
* condition, false otherwise.
*/
function matchesBelow(condition, value) {
conditionParts = condition.split(' ');
if (conditionParts.length != 2) {
return false;
}
if (conditionParts[0] != 'below') {
return false;
}
if (value < conditionParts[1]) {
return true;
}
return false;
}
/**
* Evaluates whether a value is above a threshold value.
*
* @param {string} condition The condition to be checked. (e.g. above 50).
* @param {number} value The value to be checked.
* @return {boolean} True if the value is greater than what is specified in
* condition, false otherwise.
*/
function matchesAbove(condition, value) {
conditionParts = condition.split(' ');
if (conditionParts.length != 2) {
return false;
}
if (conditionParts[0] != 'above') {
return false;
}
if (value > conditionParts[1]) {
return true;
}
return false;
}
/**
* Evaluates whether a value is within a range of values.
*
* @param {string} condition The condition to be checked (e.g. 5 to 18).
* @param {number} value The value to be checked.
* @return {boolean} True if the value is in the desired range, false otherwise.
*/
function matchesRange(condition, value) {
conditionParts = condition.replace('w+', ' ').split(' ');
if (conditionParts.length != 3) {
return false;
}
if (conditionParts[1] != 'to') {
return false;
}
if (conditionParts[0] <= value && value <= conditionParts[2]) {
return true;
}
return false;
}
/**
* Retrieves the weather for a given location, using the OpenWeatherMap API.
*
* @param {string} location The location to get the weather for.
* @return {Object.<string, string>} The weather attributes and values, as
* defined in the API.
*/
function getWeather(location) {
if (location in WEATHER_LOOKUP_CACHE) {
Logger.log('Cache hit...');
return WEATHER_LOOKUP_CACHE[location];
}
const url=`http://api.openweathermap.org/data/2.5/weather?APPID=${OPEN_WEATHER_MAP_API_KEY}&q=${location}`;
const response = UrlFetchApp.fetch(url);
if (response.getResponseCode() != 200) {
throw Utilities.formatString(
'Error returned by API: %s, Location searched: %s.',
response.getContentText(), location);
}
const result = JSON.parse(response.getContentText());
// OpenWeatherMap's way of returning errors.
if (result.cod != 200) {
throw Utilities.formatString(
'Error returned by API: %s, Location searched: %s.',
response.getContentText(), location);
}
WEATHER_LOOKUP_CACHE[location] = result;
return result;
}
/**
* Adjusts the bidModifier for a list of geo codes for a campaign.
*
* @param {string} campaignName The name of the campaign.
* @param {Array} geoCodes The list of geo codes for which bids should be
* adjusted. If null, all geo codes on the campaign are adjusted.
* @param {number} bidModifier The bid modifier to use.
*/
function adjustBids(campaignName, geoCodes, bidModifier) {
// Get the campaign.
const campaign = getCampaign(campaignName);
if (!campaign) return null;
// Get the targeted locations.
const locations = campaign.targeting().targetedLocations().get();
for (const location of locations) {
const currentBidModifier = location.getBidModifier().toFixed(2);
// Apply the bid modifier only if the campaign has a custom targeting
// for this geo location or if all locations are to be modified.
if (!geoCodes || (geoCodes.indexOf(location.getId()) != -1 &&
currentBidModifier != bidModifier)) {
Logger.log('Setting bidModifier = %s for campaign name = %s, ' +
'geoCode = %s. Old bid modifier is %s.', bidModifier,
campaignName, location.getId(), currentBidModifier);
location.setBidModifier(bidModifier);
}
}
}
/**
* Adjusts the bidModifier for campaigns targeting by proximity location
* for a given weather location.
*
* @param {string} campaignName The name of the campaign.
* @param {string} weatherLocation The weather location for which bids should be
* adjusted. If null, all proximity locations on the campaign are adjusted.
* @param {number} bidModifier The bid modifier to use.
*/
function adjustProximityBids(campaignName, weatherLocation, bidModifier) {
// Get the campaign.
const campaign = getCampaign(campaignName);
if(campaign === null) return;
// Get the proximity locations.
const proximities = campaign.targeting().targetedProximities().get();
for (const proximity of proximities) {
const currentBidModifier = proximity.getBidModifier().toFixed(2);
// Apply the bid modifier only if the campaign has a custom targeting
// for this geo location or if all locations are to be modified.
if (!weatherLocation ||
(weatherNearProximity(proximity, weatherLocation) &&
currentBidModifier != bidModifier)) {
Logger.log('Setting bidModifier = %s for campaign name = %s, with ' +
'weatherLocation = %s in proximity area. Old bid modifier is %s.',
bidModifier, campaignName, weatherLocation, currentBidModifier);
proximity.setBidModifier(bidModifier);
}
}
}
/**
* Checks if weather location is within the radius of the proximity location.
*
* @param {Object} proximity The targeted proximity of campaign.
* @param {string} weatherLocation Name of weather location to check within
* radius.
* @return {boolean} Returns true if weather location is within radius.
*/
function weatherNearProximity(proximity, weatherLocation) {
// See https://en.wikipedia.org/wiki/Haversine_formula for details on how
// to compute spherical distance.
const earthRadiusInMiles = 3960.0;
const degreesToRadians = Math.PI / 180.0;
const radiansToDegrees = 180.0 / Math.PI;
const kmToMiles = 0.621371;
const radiusInMiles = proximity.getRadiusUnits() == 'MILES' ?
proximity.getRadius() : proximity.getRadius() * kmToMiles;
// Compute the change in latitude degrees for the radius.
const deltaLat = (radiusInMiles / earthRadiusInMiles) * radiansToDegrees;
// Find the radius of a circle around the earth at given latitude.
const r = earthRadiusInMiles * Math.cos(proximity.getLatitude() *
degreesToRadians);
// Compute the change in longitude degrees for the radius.
const deltaLon = (radiusInMiles / r) * radiansToDegrees;
// Retrieve weather location for lat/lon coordinates.
const weather = getWeather(weatherLocation);
// Check if weather condition is within the proximity boundaries.
return (weather.coord.lat >= proximity.getLatitude() - deltaLat &&
weather.coord.lat <= proximity.getLatitude() + deltaLat &&
weather.coord.lon >= proximity.getLongitude() - deltaLon &&
weather.coord.lon <= proximity.getLongitude() + deltaLon);
}
/**
* Finds a campaign by name, whether it is a regular, video, or shopping
* campaign, by trying all in sequence until it finds one.
*
* @param {string} campaignName The campaign name to find.
* @return {Object} The campaign found, or null if none was found.
*/
function getCampaign(campaignName) {
const selectors = [AdsApp.campaigns(), AdsApp.videoCampaigns(),
AdsApp.shoppingCampaigns()];
for (const selector of selectors) {
const campaignIter = selector.
withCondition(`CampaignName = "${campaignName}"`).
get();
if (campaignIter.hasNext()) {
return campaignIter.next();
}
}
return null;
}
/**
* DO NOT EDIT ANYTHING BELOW THIS LINE.
* Please modify your spreadsheet URL and API key at the top of the file only.
*/
/**
* Validates the provided spreadsheet URL to make sure that it's set up
* properly. Throws a descriptive error message if validation fails.
*
* @param {string} spreadsheeturl The URL of the spreadsheet to open.
* @return {Spreadsheet} The spreadsheet object itself, fetched from the URL.
* @throws {Error} If the spreadsheet URL hasn't been set
*/
function validateAndGetSpreadsheet(spreadsheeturl) {
if (spreadsheeturl == 'INSERT_SPREADSHEET_URL_HERE') {
throw new Error('Please specify a valid Spreadsheet URL. You can find' +
' a link to a template in the associated guide for this script.');
}
const spreadsheet = SpreadsheetApp.openByUrl(spreadsheeturl);
return spreadsheet;
}
/**
* Validates the provided API key to make sure that it's not the default. Throws
* a descriptive error message if validation fails.
*
* @throws {Error} If the configured API key hasn't been set.
*/
function validateApiKey() {
if (OPEN_WEATHER_MAP_API_KEY == 'INSERT_OPEN_WEATHER_MAP_API_KEY_HERE') {
throw new Error('Please specify a valid API key for OpenWeatherMap. You ' +
'can acquire one here: http://openweathermap.org/appid');
}
}