counter = 1 # Starting condition: counter is at 1

print("The while loop is ready to begin.")

while counter <= 10: # Test the condition
    print("The counter is at: " + str(counter))
    counter += 1 # Increment the counter
    
print("The while loop has ended.")


"""
Explanation: Notebook-9: Loops and Iteration
Lesson Content

Two Ways to Iterate
While Statement
While Loop
Break and Continue
Loop over a list   


For Loop   
Code (Applied Geo-example)

In this lesson we cover the concept of iteration, which is basically the idea of repeating the same set of instructions until a certain condition is met. This sequence of instructions is also called a loop.   
This is an extremely important and powerful concept, as it allows us to finally automate tasks! Remember that a fundamental feature of programmers is that they are lazy (refresh Larry Wall's "Three Virtues" that we saw in the first notebook!). The more you can delegate to the machine and avoid repeating boring and repetitive tasks yourself, the better!
Of course, as the fantastic Randall Munroe likes to remind us, reality sometimes challenges this idea...

Two Ways to Iterate
The two most common ways to iterate (or repeat) a set of commands are the while loop and the for loop. That's why in Python the words while, for (and in) are reserved words and you can't use them for variable names. 
The video below is a nice (if slightly scary) intro to each of the approaches; you might remember them the next time you're in the gym:

The 'right' approach you use usually depends on what sort of data structure you are iterating over and whether we know before we start how many times we want to repeat the commands. That might sound a little vague at this point, but come back and have a think about it once you have a better understanding of how loops work.
So let's first see how while loops work, then we'll look at actual loops in Python.
WHILE Loops
Remember if statements? When the Python interpreter finds an if statement in your code, it checks if the specified condition evalutes to True. If the condition is True then it runs the remainder of the indented code block following the if. The 'true' block of code is only run once.
Using the while statement, the 'true' block is run for as long as the condition evalutes to True. So if the statment continues to evaluation to True then the block of code is run again and again and again and again... and again until some stopping condition is reached (usually the while condition becomes False for 'some reason').
This allows us to finally do some interesting stuff in code:
End of explanation
"""


otherCounter = 1

while ??? <= ???:
    print("This is an odd number: " + str(???))
    otherCounter += 2

otherCounter = 1

while otherCounter <= 10:
    print("This is an odd number: " + str(otherCounter))
    otherCounter += 2

"""
Explanation: If that looks confusing, don't worry! It's perfectly normal as that's your first example of iteration. 
Let's take a deeper look:
- First, we defined a variable counter and we initialised it with a value of 1. 
- Then we used the while statement to check if the value of counter was less than or equal to 10. 
- Since that condition evaluated to True, we printed the value of counter and then added 1 to the counter. 
- The indendented block of code was then run again and again by Python until the while statement returned a False (this happened because the value of counter went all the way to 11).
- After that Python simply continued to execute the code outside the while block (i.e. the last line of non-indented code)
The fundamental idea is thus: while this condition holds, repeat the instructions in the indented code block.
Many beginner programmers get confused by the fact that the value of counter is increasing without the code advancing to the point where in prints out 'The while loop has ended'. Remeber: we are starting the instruction from the beginning of the while block. So the first time that we hit it, counter has value of 1 because this is what we set it to outside of the while loop. 
But the second time the while conditional is evaluted, counter has been incremented to 2 because the last line of the while block is counter += 1! So we increased its value while inside the while block. At the beginning of the third iteration it will have a value of 3, while at its end it will be incremented to 4. And so on...
CAVEAT: pay attention to how you write your while loops! They can potentially run forever (for as long as the condition they are evaluating is True) maxing out your machine's memory. For example:
```python
don't run this!
or if you do, save everything first
and then be prepared to stop the code execution manually
(usually by pressing CTRL+D or Cmd+D)
in the terminale/console
while True:
    print("Forever Loop! Yeeee!")
```
A challenge for you!
Complete the code below to run a while loop that prints only odd numbers under 10.
End of explanation
"""


myThirdCounter = 10

while myThirdCounter >= 0:
    print("Decrementing counter: " + str(myThirdCounter))
    myThirdCounter -= 1

"""
Explanation: You can also run while loops decrementally until they meet a certain condition (run the code block to see how):
End of explanation
"""


myFourthCounter = 1

while myFourthCounter < 10:
    
    print(str(myFourthCounter))
    myFourthCounter += 1
    
    if myFourthCounter == 5:
        print("Time to escape this madness!")
        break

"""
Explanation: Break and Continue
To prematurely interrupt the execution of a while loop (before the while condition become false) you can use the break statement
End of explanation
"""


i=0
while True:
    i += 1

    if i % 2 != 0:
        print( str(i) + " is odd.")
        if i == 9:
            print("\t i is 9")
        else:
            print("\t i is not 9")
    else:
        print( str(i) + " is even.")
        if i % 4 == 0:
            print("\t i is divisible by 4.")
        elif i == 8:
            print("\t i is 8")

    if i == 21:
        break

"""
Explanation: Nesting Conditions
That last example shows that you can 'nest' an if (if myFourthCounter) inside of a while loop -- this approach allows us to add much more complex logic to our code than we could before. Here's an example, but see if you can figure out what it will print out (and how) before you run the code block!
End of explanation
"""


# So use i and j as counters because 
# this is a stylistic convention and 
# helps you to to write more concise code
# (and be constructively lazy).

i = 0

while True:
    
    i += 1
    
    if i % 2 != 0:
        print(i)
    else:
        continue
    
    if i % 7 == 0:
        print("WOOT!")
    
    if i == 25:
        break
    
print("Done!")

"""
Explanation: In terms of explanation:


Python is going to keep running the code (while True) until it is told to stop by a break (which happens at i == 21). In fact, if you remove the break then you will crash jupyter because the computer will print out every even number to infinity (which the computer can’t handle because it runs out of memory).


'Inside' the while loop there is a main if/else block:
if i % 2 != 0:
    ... do something with odd numbers...
else:
    ... do something else with even numbers...


'Inside' the odd numbers section we now have a second if/else block:
if i == 9:
    ... do something if the odd number is 9...
else:
    ... do something else if the odd number is not 9...


And it’s a similar story in the even numbers section:
if i % 4 == 0:
    ... do something if the even number is divisible by 4...
elif i == 8
    ... do something else if the even number is 8...
We'll not give you the answer to how to print out WOOT in place of 7 below, but at this point you all the clues you need. It’s the concept that is the hard part and following what’s going on when you start to nest conditions inside of conditions. We can talk this through more if anyone needs more help getting to grips with this!


i and j in Loops
If you want to skip to the next iteration then you can continue the exection of a while loop using the continue statement. In the following example we are going to skip all even numbers and print WOOT! if we hit a lucky 7 (or any number divisible by 7). We'll break out of the loop after hitting the 21<sup>st</sup> iteration.
One other thing to notice is that we've switched from looooong counter names like myFourthCounter to i. A common programming trick (which is well-known and so actually increases the legibility of your code to others) is to use i and j for counters in loops (you can add k if you ever need a third level loop).
End of explanation
"""


i = 0

while True:
    
    i += 1
    
    if i % 2 != 0:
        if i % 7 == 0:
            print("WOOT!")
        else:
            print(i)
    
    if i == 25:
        break
    
print("Done!")

"""
Explanation: How would you change the code above so that it printed only the odd number or 'WOOT!', but not both? In other words, change the code so that it prints:
1
3
5
WOOT!
9
11
13
15
17
19
WOOT!
End of explanation
"""


i = 0
while True:
    i += 1
    
    if i % 2 != 0:
        continue
    if i == ???:
        break
    print(???)

print("Done!")

i = 0
while True:
    i += 1
    
    if i % 2 != 0:
        continue
    if i == 22:
        break
    print(i)

print("Done!")

"""
Explanation: A challenge for you!
Now, replace the ??? in the code below and use it to print only even numbers less than 22.
End of explanation
"""


# remember our friends, the british computer scientists?
britishCompList = ["babbage", "lovelace", "turing"]

# this is the condition python is going to check against
stoppingCondition = len(britishCompList)

counter = 1
while counter < stoppingCondition:
    print(britishCompList[counter] + " was a british computer scientist")
    # don't forget to increment the counter!!!
    counter += 1

"""
Explanation: Iterating over a List
What you just saw with the while statement is a way of iterating: a way of repeating a certain set of instruction until a given condition is met. We can use to our advantage not only to print stuff, but also to 'iterate over' the elements in a list:
End of explanation
"""


#your code here

counter = 0 # To include Babbage, we need to start counter from 0
while counter < len(britishCompList):
    print(britishCompList[counter] + " was a british computer scientist")
    # don't forget to increment the counter!!!
    counter += 1

"""
Explanation: Wow, lot of stuff in that chunk of code, eh? Well, once again, take a deep breath and go through it line by line.
The important bits are:
- notice that this time we used the len of britishCompList as stopping condition, instead of specifying ourselves a number.
-  we accessed the items in the list with a regular index, like we have done in the past. The difference is that this time the index was the variable counter, as at each iteration counter assumes the value of 0, 1 ... until the stopping condition is met. This is equivalent to writing :
python
print(britishCompList[0])  # on the first iteration
print(britishCompList[1])  # on the second iteration
A challenge for you!
But wait a second... what about the great Babbage? Why isn't his name displayed? Certainly not because he's not worth a mention! Can you spot the reason why the iteration skipped him? Can you fix the code to include Babbage?
Hint: check (using print) the values of counter and britishCompList. What is the condition we are asking Python to use?
End of explanation
"""


counter = ???
nonBritishProgrammers = ["Torvald", "Knuth", "Swartz"]
stoppingCondition = len(???)

while counter < stoppingCondition :
    print("This is a computer genius too: " + nonBritishProgrammers[counter])
    #  always remember to increment the counter!!!
    counter += 1

counter = 0
nonBritishProgrammers = ["Torvald", "Knuth", "Swartz"]
stoppingCondition = len(nonBritishProgrammers)

while counter < stoppingCondition :
    print("This is a computer genius too: " + nonBritishProgrammers[counter])
    #  always remember to increment the counter!!!
    counter += 1

"""
Explanation: Other Overlooked Computers
If you think that being skipped over in the loop above was tough for old Babbage, then perhaps you might be interested to hear about the history of the NASA 'computers' who helped put a man on the moon and are only getting a film made about them in 2016.

A challenge for you!
Complete the following code:
End of explanation
"""


counter = 0
nonBritishProgrammers = ["Torvald", "Knuth", "Swartz"]
stoppingCondition = len(nonBritishProgrammers) -1

while counter <= stoppingCondition :
    print("These are geniuses too! " + nonBritishProgrammers[counter])
    #  always remember to increment the counter!!!
    counter +=1

"""
Explanation: CAVEAT: An important condition to remember when iterating over a list is thus that lists are zero-indexed! If if you start you counter from 1 you will certainly miss the first item in the list (which has an index of 0). 
But watch out! There's more:
Another challenge for you!
Can you guess why I needed to subtract -1 to the list's len? 
[Hint: Check the condition again. Is the same as before? (Run the code below before continuing)]
End of explanation
"""


for programmer in britishCompList:
    print(programmer)

"""
Explanation: We can see that from the code above the while condition is slightly different:
    while counter &lt; stoppingCondition : versus while counter &lt;= stoppingCondition
Because len counts the number of elements, if we use &lt;= we will access the variable counter at each iteration counter with the value of 0, 1, 2, 3. This would result in an indexError since we only have three variables.
FOR Loop
We've just seen that we can use a while loop to iterate over a list, but it's kind of clunky and inelegant. All those counters and things makes for a lot of extra typing, especially when you consider how terse Python's language usually is! Surely there must be another way?
You guessed right, my friend! Let me introduce you to the 'for ... in:' statement:
End of explanation
"""


# WHILE LOOP
whileCounter = 0
myList = [0,1,2,3,4]
stoppingCondition = len(myList)
while whileCounter < stoppingCondition:
    print("Element number: " + str(myList[whileCounter]))
    whileCounter +=1 

# FOR LOOP
for element in myList:
     print("Element number: " + str(element))

"""
Explanation: As you can see, the for loop statement is much more concise: you simply tell Python to repeat a certain instruction (print the list item in this example) for EVERY ITEM in A SEQUENCE. The sequence here is the list of British computer scientists britishCompList created in the code block above.
Now, Python will stop automatically when the sequence is finished without you having to worry about specifying the stopping condition (which you have to do when using a while loop).
Notice also that we didn't have to initialise the counter value!
So, the biggest difference between a while and a for loop is thus not merely stylistic, it's also conceptual!
Let's recap with another example:
End of explanation
"""


numbers = [1,2,3,4,5,6,7,8,9,10]
for n in numbers:
    if (???):
        print(n)

numbers = [1,2,3,4,5,6,7,8,9,10]
for n in numbers:
    if (n % 2 != 0):
        print(n)

"""
Explanation: SIDENOTE: See how the value of myList[whileCounter] and that of element in the two loops are the same? That's  because Python is doing the indexing job for you behind the scenes.
A challenge for you!
Print only the odd numbers in the list. HINT: remember the modulo operator?
End of explanation
"""


for i in range(10):
    if (i % 2 != 0):
        print(i)

"""
Explanation: Now, as we are lazy programmers, let's repeat the above example combining the range function with a for loop. It will save us the hassle of typing all those numbers!
End of explanation
"""


programmers = {
    "Charles": "Babbage",
    "Ada": "Lovelace",
    "Alan":"Turing"
}
for k in programmers:
    print(k)

"""
Explanation: Cool, we have seen how to iterate over a list, but what about a dictionary? Well, if you remember we said that you might think of a dictionary as a kind of list where each element isn't indexed by an integer, but rather by a unique identifier (key).
Hence, as with lists where we iterate over the indexes, with dictionaries we are going to iterate over the keys!
End of explanation
"""


for k,v in programmers.items():
    print("this is the value: \'" + v + "\' for the key: \'" + k +"\'")

"""
Explanation: NOTE: I've used the variable k. This is simply an arbitrary word that I've choosen and not some kind of special variable. You could have used anyRandomNameForWhatMatters. 
What if you want to retrieve the values? In that case you should use not only two variables, (the first for the keys and the second for the values) but also invoke the method items() on the dictionary, like so:
End of explanation
"""


KCL_marker = {
      "type": "Feature",
      "properties": {
        "marker-color": "#7e7e7e",
        "marker-size": "medium",
        "marker-symbol": "",
        "name": "KCL"
      },
      "geometry": {
        "type": "Point",
        "coordinates": [
          -0.11630058288574219,
          51.51135999349117
        ]
      }
    }
for ???,v in KCL_marker["???"].items():
    print("KCL_marker has a property: '" + ??? + "' for the key: " + k)


KCL_marker = {
      "type": "Feature",
      "properties": {
        "marker-color": "#7e7e7e",
        "marker-size": "medium",
        "marker-symbol": "",
        "name": "KCL"
      },
      "geometry": {
        "type": "Point",
        "coordinates": [
          -0.11630058288574219,
          51.51135999349117
        ]
      }
    }
for k,v in KCL_marker["properties"].items():
    print("KCL_marker has a property: '" + v + "' for the key: " + k)


"""
Explanation: A Challenge for you!
Iterate over the GeoJSON marker and print its "properties".
End of explanation
"""


# define a new featureCollection: it is basically a very fancy dictionary
# to which we are going to add new 'features' (which are points on a map
# but represented as *data* by a dictionary). We need to add one feature
# at a time when building our transect...
# initial coordinate list
init_coords = [-0.0200, 51.592]
# dX delta 
dx  = 0.03
gap = 0

transect = {
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {},
      "geometry": {
        "type": "Point",
        "coordinates": init_coords
      }
    }
# -------------------------------------------------------------
#  here is where the remaining three points have to be
#  added using *code* and not manually
# -------------------------------------------------------------
  ]
}

# new empty list where I'm going to put all the new dictionaries 
# a.k.a. all the new points
three_new_points = []

for i in range(3):
#   define a new point 
    new_point = {
      "type": "Feature",
      "properties": {},
      "geometry": {
        "type": "Point",
        "coordinates": []

      }
    }
    
    # increment the longitude
    gap += dx
    
    # create a new list with the updated coordinates
    new_coordinates = [init_coords[0] + gap, init_coords[1]]

    # assign the new coordinates to the coordinates key
    # in the new point dictionary
    new_point["geometry"]["coordinates"] = new_coordinates
    new_point["properties"]["name"] = "Point " + str(i+1)

    # append the new point dictionary to the list of new points
    three_new_points.append(new_point)


# append to the feature list the three new points
# that we created
transect["features"].extend(three_new_points)
print(transect)

"""
Explanation: Very good! Let's summarise some facts about loops:

you can increment the counter
but also decrement it (effectively counting down!)
the increment doesn't need to be 1 every the time (you can increment by 2, 50, whatever..)
don’t forget to to indent the block of code after the colon!

Code (Applied Geo-example)
The geo-excercise I'll give you this time is a real-world problem that you might face one day in your career as geospatial professionals. In principle it is possible to work out everything below based on what we've done up to this point; however, this exercise is also hard since it is a big jump conceptually that mixes up the ideas in a new way. So if you can't quite make out the answer don't worry, just try to understand the concepts above and see if you can solve parts of the problem. 
Let's say a colleague of yours used a GPS to survey at regular intervals the dispersion of pollutants in a patch of terrain. Unfortunately, after a good start they forgot to record all the remaining points! 
But that's not a terrible problem, as the transect has a perfect West-East orientation and direction, and all the points are spaced by a the same value dX (short for 'delta-X', the change in X between each point) of 0.03 degrees longitude, i.e.:
(0.0102, 51.592)-----(X+dX,Y)-----(X+2dX,Y)-----(X+3dX,Y)--->
Using what we've seen so far, try to create a GeoJSON featureCollection of points. To give you a head start, I've provided some scaffolding.
HINT: Being the skilled geographer that you are, you immediately realise that actually you've got all the coordinates that you need, even for the missing points (i.e. the latitude values will remain constant...)
End of explanation
"""


import json 
parsed = json.loads(str(transect).replace("\'", "\""))
print(json.dumps(parsed, indent=4))

from ipyleaflet import Map, GeoJSON, basemaps
m = Map(center = (51.51, -0.10), zoom=10, min_zoom=5, max_zoom=20, 
   basemap=basemaps.OpenTopoMap)
geo = GeoJSON(data=parsed)
m.add_layer(geo)
m

"""
Explanation: This output on its own makes very little sense, but it's actually a real world data structure called JSON. Below we use a handy library (that you might not yet have installed on your computer) to turn that JSON into something easier to read, and then we'll see how you can use it!
End of explanation
"""


