HourTrip – The Technical report

Update 2020/06/03 Well, we’ve finally ended it, 1.5 years collecting data, taking the photo of every single location, and making the MVP went down the drain.

But worry not, although I’m getting older, this will not be my last venture, as long I can work 16-18 hours a day someday I will have a business.

Hourtrip is a travel website that I did with a few of my friends although we are still struggling on the business side, I’m quite proud of the algorithm.

Have you gone on a trip and thought of “I only have 6 hours to spend where should I go?” well with Hourtrip you can! 

The concept is pretty simple, have you gone on a trip and thought of “I only have 6 hours to spend where should I go?” well with Hourtrip you can! and it’s smart enough to know whether it’s time for lunch or dinner, which places you should go to, determine what time you should go back, etc.

As an overview, the algorithm is pretty simple. Extract the user’s preference, use that data to filter the database, then show the recommended locations.

To show the recommended locations, it needs to iteratively calculate the closest location from the original location, the graph below explains roughly how the iteration works.

Check the weather
I use OpenWeatherMap API to get the info from, at this point, I only check whether it’s bad or good, in the future, I’m planning to check whether the weather is good enough for traveling.

For example, a drizzle might not stop you from going to that restaurant you like.

URL = "http://api.openweathermap.org/data/2.5/forecast/?units=metric&APPID=0cdde5a888c7dc5664225b6112dcd07d"
path = URL + "&lat=" + kwargs['origin']['lat'] + "&lon=" + kwargs['origin']['lng']
result = json.loads(urlopen(path).read().decode('utf-8'))

Check the preferences
Checking the preferences is simply a hash lookup.

if self.match_tags(self._types, v['tag']):
  di = DestinationItem(data=v)

Check the walking duration and Location’s tiers
There are two components when checking the walking duration, the time to get from the current location to the next location, and the current location to the original point.

Determining the walking speed is quite tricky and I have to admit is a mixture of real data and hunch, according to Wikipedia, the average walking speed is 1.4 m/s but for tourists, they might walk longer than that, due to the traffic, pausing for the sights, having unruly kids, etc. so I add another 1 second to anticipate that.

WALKING_SPEED   = 2.4  #  +1 to anticipate sight seeing, traffic, etc.

def get_distance(self, node_origin, node_dest):

    origin  = self.get_latlng(node_origin)
    dest    = self.get_latlng(node_dest)
    dist    = round(geodesic(origin, dest).kilometers * 1000)

    return {
        'dist_from_source': dist, # meters
        'walking_time': round(dist/WALKING_SPEED)

In the beginning, to calculate the nearest distance I use Google Map API, but boy was that expensive! Nearly bankrupt all of us so I use GeoPy library instead.

Once the walking duration is understood it’s easy to get the nearest location, now this has to be tied with the location’s tier (score value).

def get_shortest_dest(self, node_origin, tier, p_dist=None):

  dist    = None
  index   = -1
  arrival = None

  for i, v in enumerate(self._dest_list):

    if self.match_tags([v.tier], tier):

      d = self.get_distance(node_origin,v)
      # estimated arrival times
      # TODO: there's a variable for arrival_time, change tothat
      arrival = self._timestamp + datetime.timedelta(seconds=d['walking_time'])

      if v.is_open(arrival):

        if dist is None:
          dist = d['dist_from_source']

        if p_dist is None:

          if d['dist_from_source'] <= dist:
            dist    = d['dist_from_source']
            index   = i
          # if between dest_a and dest_b subtier exist then calculate
          if d['dist_from_source'] <= dist and d['dist_from_source'] <= p_dist:
            dist    = d['dist_from_source']
            index   = i

  return { 'index': index, 'dist': dist, 'arrival': arrival}

def get_next_dest(self, node_origin):

    result  = self.get_shortest_dest(node_origin, ['1','2'])
    i       = result['index']
    ii      = -1

    # 2. if tier [1,2] undefined, search [3,4]
    if i < 0:
        result  = self.get_shortest_dest(node_origin, ['3','4'])
        ii      = result['index']

        # if tier [1,2] exist, search for the closest [3,4] tier
        # since the last tier has already been removed, then automatically the next tier will be refered
        result  = self.get_shortest_dest(node_origin, ['3','4'], result['dist'])
        ii      = result['index']

    # return tier 3/4
    if ii >= 0 and len(self._result_dest) > 0:

        tmp_tier = self._dest_list[ii].tier
        tmp_all_tier = self._main_tmp.get_all_tier()

        # if tmp_tier not in tmp_all_tier:
        #     # ensure that each leg has only 1 3/4 tier
        #     self._main_tmp.add_tier(tmp_tier, ii)
        #     return self._dest_list.pop(ii)

        # TODO: this repetition, refactor
        if self._dest_list[ii].is_gluttony_time(result['arrival']):

            if tmp_tier not in tmp_all_tier:
                self._main_tmp.add_tier(tmp_tier, ii)
                return self._dest_list.pop(ii)


            if tmp_tier not in tmp_all_tier:
                self._main_tmp.add_tier(tmp_tier, ii)
                return self._dest_list.pop(ii)

    if i >= 0:
        # 1. save it to the main_temp for refference
        self._main_tmp = self._dest_list[i]
        return self._dest_list.pop(i)

    return None

Each trip will be distributed between 1 premium locations’ tier and 3 ordinary location’s tier so if the user still has time to visit other places then the same pattern will be repeated.

// Calculating the time budget

if(walking time + spending time + going home time < time budget) {
} else {
   return result;

And of course, the pattern may vary depending on distances and schedules.

Check schedules
There are 3 schedules I need to check, locations’ schedule, public transport schedule, and whether it’s time for lunch/dinner.

Checking it is a simple lookup table comparison.

def is_gluttony_time(self, time):

    # Restaurant 1100-1400, 1700-2100
    # Cafe 0700-1100, 1300-1700
    # Liquor 1700-2600
    time_int = int(time.strftime('%H%M'))
    time_slot = [
            'open': [1100, 1700],
            'close': [1400, 2100],
            'tag': ['restaurant']
            'open': [700, 1300],
            'close': [1100, 1700],
            'tag': ['restaurant','cafe']
            'open': [1700],
            'close': [2600],
            'tag': ['liquor']

    for i, data in enumerate(time_slot):

        a_open   = data['open']
        a_close  = data['close']

        for j, v in enumerate(a_open):
            if time_int > v and time_int < a_close[j]:

                # TODO: change self._types to tag
                check = self.match_tags(time_slot[j]['tag'], self._types)

                if check:
                    self._raw['is_gluttony'] = True

                return check

    return False

So that’s the gist of it, I still have the plan to expand it, find some investors and do what all startup founders do drown in money and be famous or have a mental breakdown, hahaha.

Icons made by dDara from www.flaticon.com
Icons made by catkuro from www.flaticon.com
Icons made by Smashicons from www.flaticon.com

Leave a Reply

Your email address will not be published. Required fields are marked *