Containerization basics
| @ -1,30 +0,0 @@ | ||||
| from flask import Flask, render_template | ||||
| import os | ||||
| import random | ||||
| 
 | ||||
| app = Flask(__name__) | ||||
| 
 | ||||
| # list of cat images | ||||
| images = [ | ||||
|     "https://firebasestorage.googleapis.com/v0/b/docker-curriculum.appspot.com/o/catnip%2F0.gif?alt=media&token=0fff4b31-b3d8-44fb-be39-723f040e57fb", | ||||
|     "https://firebasestorage.googleapis.com/v0/b/docker-curriculum.appspot.com/o/catnip%2F1.gif?alt=media&token=2328c855-572f-4a10-af8c-23a6e1db574c", | ||||
|     "https://firebasestorage.googleapis.com/v0/b/docker-curriculum.appspot.com/o/catnip%2F10.gif?alt=media&token=647fd422-c8d1-4879-af3e-fea695da79b2", | ||||
|     "https://firebasestorage.googleapis.com/v0/b/docker-curriculum.appspot.com/o/catnip%2F11.gif?alt=media&token=900cce1f-55c0-4e02-80c6-ee587d1e9b6e", | ||||
|     "https://firebasestorage.googleapis.com/v0/b/docker-curriculum.appspot.com/o/catnip%2F2.gif?alt=media&token=8a108bd4-8dfc-4dbc-9b8c-0db0e626f65b", | ||||
|     "https://firebasestorage.googleapis.com/v0/b/docker-curriculum.appspot.com/o/catnip%2F3.gif?alt=media&token=4e270d85-0be3-4048-99bd-696ece8070ea", | ||||
|     "https://firebasestorage.googleapis.com/v0/b/docker-curriculum.appspot.com/o/catnip%2F4.gif?alt=media&token=e7daf297-e615-4dfc-aa19-bee959204774", | ||||
|     "https://firebasestorage.googleapis.com/v0/b/docker-curriculum.appspot.com/o/catnip%2F5.gif?alt=media&token=a8e472e6-94da-45f9-aab8-d51ec499e5ed", | ||||
|     "https://firebasestorage.googleapis.com/v0/b/docker-curriculum.appspot.com/o/catnip%2F7.gif?alt=media&token=9e449089-9f94-4002-a92a-3e44c6bd18a9", | ||||
|     "https://firebasestorage.googleapis.com/v0/b/docker-curriculum.appspot.com/o/catnip%2F8.gif?alt=media&token=80a48714-7aaa-45fa-a36b-a7653dc3292b", | ||||
|     "https://firebasestorage.googleapis.com/v0/b/docker-curriculum.appspot.com/o/catnip%2F9.gif?alt=media&token=a57a1c71-a8af-4170-8fee-bfe11809f0b3", | ||||
| ] | ||||
| 
 | ||||
| 
 | ||||
| @app.route("/") | ||||
| def index(): | ||||
|     url = random.choice(images) | ||||
|     return render_template("index.html", url=url) | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
|     app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 5000))) | ||||
| @ -1 +0,0 @@ | ||||
| Flask==2.0.2 | ||||
| @ -1,27 +0,0 @@ | ||||
| <html> | ||||
|   <head> | ||||
|     <style type="text/css"> | ||||
|       body { | ||||
|         background: black; | ||||
|         color: white; | ||||
|       } | ||||
|       div.container { | ||||
|         max-width: 500px; | ||||
|         margin: 100px auto; | ||||
|         border: 20px solid white; | ||||
|         padding: 10px; | ||||
|         text-align: center; | ||||
|       } | ||||
|       h4 { | ||||
|         text-transform: uppercase; | ||||
|       } | ||||
|     </style> | ||||
|   </head> | ||||
|   <body> | ||||
|     <div class="container"> | ||||
|       <h4>Cat Gif of the day</h4> | ||||
|       <img src="{{url}}" /> | ||||
|       <p><small>Courtesy: <a href="http://www.buzzfeed.com/copyranter/the-best-cat-gif-post-in-the-history-of-cat-gifs">Buzzfeed</a></small></p> | ||||
|     </div> | ||||
|   </body> | ||||
| </html> | ||||
| @ -0,0 +1,33 @@ | ||||
| from flask import Flask, render_template, make_response, send_file | ||||
| import os | ||||
| import random | ||||
| 
 | ||||
| app = Flask(__name__) | ||||
| 
 | ||||
| req_count = 0 | ||||
| 
 | ||||
| static_dir = os.path.join(os.path.dirname(__file__), 'static/') | ||||
| 
 | ||||
| @app.route('/static/<path:path>') | ||||
| def static_file(path): | ||||
|     return send_file(os.path.join(static_dir, path)) | ||||
| 
 | ||||
| @app.route("/") | ||||
| def index(): | ||||
|     global req_count | ||||
|     cat_files = os.listdir(os.path.join(static_dir, 'img/')) | ||||
|     file = random.choice(cat_files) | ||||
|     is_stoner = os.path.basename(file) == 'stoner_cat.gif' | ||||
|     url = '/static/img/' + file | ||||
|     req_count += 1 | ||||
|     return render_template("index.html", url=url, is_stoner=is_stoner) | ||||
| 
 | ||||
| @app.route("/request_count") | ||||
| def request_count(): | ||||
|     response = make_response(str(req_count), 200) | ||||
|     response.mimetype = "text/plain" | ||||
|     return response | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
|     app.config['TEMPLATES_AUTO_RELOAD'] = True | ||||
|     app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 5000))) | ||||
| @ -0,0 +1,3 @@ | ||||
| #!/bin/sh | ||||
| 
 | ||||
| pip install -r ./requirements.txt | ||||
| @ -0,0 +1,51 @@ | ||||
| user nginx; | ||||
| worker_processes auto; | ||||
| 
 | ||||
| error_log  /var/log/nginx/error.log notice; | ||||
| pid        /var/run/nginx.pid; | ||||
| 
 | ||||
| events { | ||||
|     worker_connections 1024; | ||||
| } | ||||
| 
 | ||||
| http { | ||||
|     include       /etc/nginx/mime.types; | ||||
|     default_type  application/octet-stream; | ||||
| 
 | ||||
|     log_format  main  '$remote_addr - $remote_user [$time_local] "$request" ' | ||||
|                       '$status $body_bytes_sent "$http_referer" ' | ||||
|                       '"$http_user_agent" "$http_x_forwarded_for"'; | ||||
| 
 | ||||
|     access_log  /var/log/nginx/access.log  main; | ||||
| 
 | ||||
|     sendfile        on; | ||||
|     #tcp_nopush     on; | ||||
| 
 | ||||
|     #gzip  on; | ||||
| 
 | ||||
|     keepalive_timeout  65; | ||||
| 
 | ||||
|     server { | ||||
|         listen 80; | ||||
| 
 | ||||
|         location / { | ||||
|             proxy_set_header X-Forwarded-Host $http_host; | ||||
|             proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; | ||||
|             proxy_set_header X-Forwarded-Proto $scheme; | ||||
| 
 | ||||
|             proxy_buffers 8 64k; | ||||
|             proxy_busy_buffers_size 128k; | ||||
|             proxy_buffer_size 64k; | ||||
| 
 | ||||
|             client_max_body_size 10m; | ||||
| 
 | ||||
|             proxy_http_version 1.1; | ||||
|             proxy_pass http://cats_app:5000; | ||||
|         } | ||||
| 
 | ||||
|         location /request_count { | ||||
|             deny all; | ||||
|             return 404; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1 @@ | ||||
| Flask==3.0.0 | ||||
| @ -0,0 +1,3 @@ | ||||
| #!/bin/sh | ||||
| 
 | ||||
| python3 ./app.py | ||||
| After Width: | Height: | Size: 372 B | 
| After Width: | Height: | Size: 894 B | 
| After Width: | Height: | Size: 2.0 MiB | 
| After Width: | Height: | Size: 6.1 MiB | 
| After Width: | Height: | Size: 1.6 MiB | 
| After Width: | Height: | Size: 124 KiB | 
| After Width: | Height: | Size: 31 MiB | 
| After Width: | Height: | Size: 6.6 MiB | 
| After Width: | Height: | Size: 6.5 MiB | 
| @ -0,0 +1,77 @@ | ||||
| <!DOCTYPE HTML> | ||||
| 
 | ||||
| <html lang="en" class="h-100"> | ||||
| 
 | ||||
| <head> | ||||
|   <meta charset="utf-8"> | ||||
|   <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||
|   <title>Cats</title> | ||||
|   <link rel="icon" type="image/png" href="/static/favicon.png"> | ||||
|   <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" | ||||
|     integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous"> | ||||
|   <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" | ||||
|     integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" | ||||
|     crossorigin="anonymous"></script> | ||||
|   <style lang="text/css"> | ||||
|     .footer { | ||||
|       position: fixed; | ||||
|       bottom: 0; | ||||
|     } | ||||
| 
 | ||||
|     .stoner-video { | ||||
|       position: fixed; | ||||
|       left: 0; | ||||
|       top: 0; | ||||
|       min-width: 100%; | ||||
|       min-height: 100%; | ||||
|     } | ||||
| 
 | ||||
|     .stoner-wave { | ||||
|       animation: wave 5s cubic-bezier(0.36, 0.45, 0.63, 0.53) infinite; | ||||
|       animation-direction: alternate; | ||||
|     } | ||||
| 
 | ||||
|     @keyframes wave { | ||||
|       0% { | ||||
|         transform: rotate(-20deg); | ||||
|         background-color: greenyellow; | ||||
|       } | ||||
| 
 | ||||
|       100% { | ||||
|         transform: rotate(20deg); | ||||
|         background-color: fuchsia; | ||||
|       } | ||||
|     } | ||||
|   </style> | ||||
| </head> | ||||
| 
 | ||||
| <body class="h-100"> | ||||
|   {% if is_stoner %} | ||||
|   <video autoplay loop class="stoner-video"> | ||||
|     <source src="/static/vid/stoner_cat.mp4" type="video/mp4" /> | ||||
|   </video> | ||||
|   {% endif %} | ||||
|   <div class="h-100 d-flex flex-row align-items-center"> | ||||
|     <div class="container"> | ||||
|       <div class="row p-3 justify-content-center"> | ||||
|         <div class="col col-lg-4"> | ||||
|           <div class="card {{ 'stoner-wave' if is_stoner }}"> | ||||
|             <img src="{{url}}" class="card-img-top" alt="cat"> | ||||
|             <div class="card-body"> | ||||
|               <button id="#next-button" class="btn btn-primary {{ 'bg-black' if is_stoner }}" | ||||
|                 onclick="location.reload()"> | ||||
|                 <img src="/static/icons/icon-paw-32.png" alt="paw" /> | ||||
|                 Gimme another cat! | ||||
|               </button> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|   <footer class="footer p-1"> | ||||
|     <span>Icons by <a href="https://icons8.com">icons8.com</a></span> | ||||
|   </footer> | ||||
| </body> | ||||
| 
 | ||||
| </html> | ||||
| @ -1,14 +0,0 @@ | ||||
| #!/bin/bash | ||||
| 
 | ||||
| # build the flask container | ||||
| docker build -t studX/foodtrucks-web ./ | ||||
| 
 | ||||
| # create the network | ||||
| docker network create foodtrucks-network | ||||
| 
 | ||||
| # start the ES container, specify password beforehand | ||||
| docker run -d --net foodtrucks-network --name elastic -e ELASTIC_PASSWORD=v7SLsbtXticPLADei5vS elasticsearch:8.4.3 | ||||
| 
 | ||||
| # start the flask app container  | ||||
| # point to elastic host: https://elastic:9200 user: elastic password: v7SLsbtXticPLADei5vS | ||||
| docker run -d -p 80:5000 --net foodtrucks-network studX/foodtrucks-web https elastic 9200 elastic v7SLsbtXticPLADei5vS | ||||
| @ -1,13 +0,0 @@ | ||||
| version: "3" | ||||
| services: | ||||
|   elastic: | ||||
|     image: elasticsearch:8.4.3 | ||||
|     environment: | ||||
|       - ELASTIC_PASSWORD=v7SLsbtXticPLADei5vS | ||||
|   foodtrucks-web: | ||||
|     image: stud15/foodtrucks-web | ||||
|     command: https elastic 9200 elastic v7SLsbtXticPLADei5vS | ||||
|     depends_on: | ||||
|       - elastic | ||||
|     ports: | ||||
|       - 80:5000 | ||||
| @ -1,3 +0,0 @@ | ||||
| { | ||||
|   "presets": ["env"] | ||||
| } | ||||
| @ -1,135 +0,0 @@ | ||||
| from elasticsearch import Elasticsearch, exceptions | ||||
| import os | ||||
| import time | ||||
| from flask import Flask, jsonify, request, render_template | ||||
| import sys | ||||
| import requests | ||||
| 
 | ||||
| es_scheme = sys.argv[1] | ||||
| es_host = sys.argv[2] | ||||
| es_port = int(sys.argv[3]) | ||||
| es_user = sys.argv[4] | ||||
| es_password = sys.argv[5] | ||||
| connection_string = f"{es_scheme}://{es_host}:{es_port}" | ||||
| 
 | ||||
| print(f"Elastic server: {es_scheme}://{es_host}:{es_port}, auth: {es_user}:{es_password}") | ||||
| 
 | ||||
| es = Elasticsearch( | ||||
|         connection_string, | ||||
|         basic_auth=(es_user, es_password), | ||||
|         verify_certs=False | ||||
| ) | ||||
| 
 | ||||
| app = Flask(__name__) | ||||
| 
 | ||||
| def load_data_in_es(): | ||||
|     """ creates an index in elasticsearch """ | ||||
|     url = "http://data.sfgov.org/resource/rqzj-sfat.json" | ||||
|     r = requests.get(url) | ||||
|     data = r.json() | ||||
|     print("Loading data in elasticsearch ...") | ||||
|     for id, truck in enumerate(data): | ||||
|         res = es.index(index="sfdata", id=id, body=truck) | ||||
|     print("Total trucks loaded: ", len(data)) | ||||
| 
 | ||||
| def safe_check_index(index, retry=60): | ||||
|     """ connect to ES with retry """ | ||||
|     if not retry: | ||||
|         print("Out of retries. Bailing out...") | ||||
|         sys.exit(1) | ||||
|     try: | ||||
|         status = es.indices.exists(index=index) | ||||
|         return status | ||||
|     except exceptions.ConnectionError as e: | ||||
|         print(f"Unable to connect to {connection_string}. Retrying in 5 secs...", flush=True) | ||||
|         time.sleep(5) | ||||
|         safe_check_index(index, retry-1) | ||||
| 
 | ||||
| def format_fooditems(string): | ||||
|     items = [x.strip().lower() for x in string.split(":")] | ||||
|     return items[1:] if items[0].find("cold truck") > -1 else items | ||||
| 
 | ||||
| def check_and_load_index(): | ||||
|     """ checks if index exits and loads the data accordingly """ | ||||
|     if not safe_check_index('sfdata'): | ||||
|         print("Index not found...") | ||||
|         load_data_in_es() | ||||
| 
 | ||||
| ########### | ||||
| ### APP ### | ||||
| ########### | ||||
| @app.route('/') | ||||
| def index(): | ||||
|     return render_template('index.html') | ||||
| 
 | ||||
| @app.route('/debug') | ||||
| def test_es(): | ||||
|     resp = {} | ||||
|     try: | ||||
|         msg = es.cat.indices() | ||||
|         resp["msg"] = msg | ||||
|         resp["status"] = "success" | ||||
|     except: | ||||
|         resp["status"] = "failure" | ||||
|         resp["msg"] = "Unable to reach ES" | ||||
|     return jsonify(resp) | ||||
| 
 | ||||
| @app.route('/search') | ||||
| def search(): | ||||
|     key = request.args.get('q') | ||||
|     if not key: | ||||
|         return jsonify({ | ||||
|             "status": "failure", | ||||
|             "msg": "Please provide a query" | ||||
|         }) | ||||
|     try: | ||||
|         res = es.search( | ||||
|                 index="sfdata", | ||||
|                 body={ | ||||
|                     "query": {"match": {"fooditems": key}}, | ||||
|                     "size": 750 # max document size | ||||
|               }) | ||||
|     except Exception as e: | ||||
|         return jsonify({ | ||||
|             "status": "failure", | ||||
|             "msg": "error in reaching elasticsearch" | ||||
|         }) | ||||
|     # filtering results | ||||
|     vendors = set([x["_source"]["applicant"] for x in res["hits"]["hits"]]) | ||||
|     temp = {v: [] for v in vendors} | ||||
|     fooditems = {v: "" for v in vendors} | ||||
|     for r in res["hits"]["hits"]: | ||||
|         applicant = r["_source"]["applicant"] | ||||
|         if "location" in r["_source"]: | ||||
|             truck = { | ||||
|                 "hours"    : r["_source"].get("dayshours", "NA"), | ||||
|                 "schedule" : r["_source"].get("schedule", "NA"), | ||||
|                 "address"  : r["_source"].get("address", "NA"), | ||||
|                 "location" : r["_source"]["location"] | ||||
|             } | ||||
|             fooditems[applicant] = r["_source"]["fooditems"] | ||||
|             temp[applicant].append(truck) | ||||
| 
 | ||||
|     # building up results | ||||
|     results = {"trucks": []} | ||||
|     for v in temp: | ||||
|         results["trucks"].append({ | ||||
|             "name": v, | ||||
|             "fooditems": format_fooditems(fooditems[v]), | ||||
|             "branches": temp[v], | ||||
|             "drinks": fooditems[v].find("COLD TRUCK") > -1 | ||||
|         }) | ||||
|     hits = len(results["trucks"]) | ||||
|     locations = sum([len(r["branches"]) for r in results["trucks"]]) | ||||
| 
 | ||||
|     return jsonify({ | ||||
|         "trucks": results["trucks"], | ||||
|         "hits": hits, | ||||
|         "locations": locations, | ||||
|         "status": "success" | ||||
|     }) | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
|     ENVIRONMENT_DEBUG = os.environ.get("DEBUG", False) | ||||
|     check_and_load_index() | ||||
|     app.run(host='0.0.0.0', port=5000, debug=ENVIRONMENT_DEBUG) | ||||
| @ -1,26 +0,0 @@ | ||||
| { | ||||
|   "name": "sf-food", | ||||
|   "version": "0.0.1", | ||||
|   "description": "SF food app", | ||||
|   "main": "index.js", | ||||
|   "scripts": { | ||||
|     "start": "webpack --progress --colors --watch", | ||||
|     "build": "NODE_ENV='production' webpack -p", | ||||
|     "test": "echo \"Error: no test specified\" && exit 1" | ||||
|   }, | ||||
|   "author": "Prakhar Srivastav", | ||||
|   "license": "MIT", | ||||
|   "dependencies": { | ||||
|     "react": "^16.13.1", | ||||
|     "react-dom": "^16.13.1", | ||||
|     "superagent": "^5.2.2" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "babel-core": "^6.3.26", | ||||
|     "babel-loader": "^6.2.0", | ||||
|     "babel-preset-env": "^1.7.0", | ||||
|     "babel-preset-es2015": "^6.3.13", | ||||
|     "babel-preset-react": "^6.3.13", | ||||
|     "webpack": "^1.12.9" | ||||
|   } | ||||
| } | ||||
| @ -1,3 +0,0 @@ | ||||
| elasticsearch===8.4.3 | ||||
| Flask==2.1.0 | ||||
| requests==2.23.0 | ||||
| Before Width: | Height: | Size: 1.1 KiB | 
| Before Width: | Height: | Size: 8.2 KiB | 
| Before Width: | Height: | Size: 11 KiB | 
| Before Width: | Height: | Size: 1.8 KiB | 
| Before Width: | Height: | Size: 2.8 KiB | 
| Before Width: | Height: | Size: 3.8 KiB | 
| Before Width: | Height: | Size: 5.2 KiB | 
| Before Width: | Height: | Size: 6.2 KiB | 
| Before Width: | Height: | Size: 6.6 KiB | 
| Before Width: | Height: | Size: 8.2 KiB | 
| Before Width: | Height: | Size: 8.8 KiB | 
| Before Width: | Height: | Size: 11 KiB | 
| Before Width: | Height: | Size: 3.2 KiB | 
| Before Width: | Height: | Size: 3.4 KiB | 
| Before Width: | Height: | Size: 3.8 KiB | 
| Before Width: | Height: | Size: 4.0 KiB | 
| Before Width: | Height: | Size: 11 KiB | 
| Before Width: | Height: | Size: 11 KiB | 
| @ -1,2 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <browserconfig><msapplication><tile><square70x70logo src="/ms-icon-70x70.png"/><square150x150logo src="/ms-icon-150x150.png"/><square310x310logo src="/ms-icon-310x310.png"/><TileColor>#ffffff</TileColor></tile></msapplication></browserconfig> | ||||
| Before Width: | Height: | Size: 1.2 KiB | 
| Before Width: | Height: | Size: 1.7 KiB | 
| Before Width: | Height: | Size: 5.2 KiB | 
| Before Width: | Height: | Size: 1.1 KiB | 
| @ -1,41 +0,0 @@ | ||||
| { | ||||
|  "name": "App", | ||||
|  "icons": [ | ||||
|   { | ||||
|    "src": "\/android-icon-36x36.png", | ||||
|    "sizes": "36x36", | ||||
|    "type": "image\/png", | ||||
|    "density": "0.75" | ||||
|   }, | ||||
|   { | ||||
|    "src": "\/android-icon-48x48.png", | ||||
|    "sizes": "48x48", | ||||
|    "type": "image\/png", | ||||
|    "density": "1.0" | ||||
|   }, | ||||
|   { | ||||
|    "src": "\/android-icon-72x72.png", | ||||
|    "sizes": "72x72", | ||||
|    "type": "image\/png", | ||||
|    "density": "1.5" | ||||
|   }, | ||||
|   { | ||||
|    "src": "\/android-icon-96x96.png", | ||||
|    "sizes": "96x96", | ||||
|    "type": "image\/png", | ||||
|    "density": "2.0" | ||||
|   }, | ||||
|   { | ||||
|    "src": "\/android-icon-144x144.png", | ||||
|    "sizes": "144x144", | ||||
|    "type": "image\/png", | ||||
|    "density": "3.0" | ||||
|   }, | ||||
|   { | ||||
|    "src": "\/android-icon-192x192.png", | ||||
|    "sizes": "192x192", | ||||
|    "type": "image\/png", | ||||
|    "density": "4.0" | ||||
|   } | ||||
|  ] | ||||
| } | ||||
| Before Width: | Height: | Size: 8.2 KiB | 
| Before Width: | Height: | Size: 8.5 KiB | 
| Before Width: | Height: | Size: 22 KiB | 
| Before Width: | Height: | Size: 3.8 KiB | 
| @ -1,50 +0,0 @@ | ||||
| import React from "react"; | ||||
| import ReactDOM from "react-dom"; | ||||
| import Sidebar from "./components/Sidebar"; | ||||
| 
 | ||||
| // setting up mapbox
 | ||||
| mapboxgl.accessToken = | ||||
|   "pk.eyJ1IjoicHJha2hhciIsImEiOiJjaWZlbzQ1M2I3Nmt2cnhrbnlxcTQyN3VkIn0.uOaUAUqN2VS7dC7XKS0KkQ"; | ||||
| 
 | ||||
| var map = new mapboxgl.Map({ | ||||
|   container: "map", | ||||
|   style: "mapbox://styles/prakhar/cij2cpsn1004p8ykqqir34jm8", | ||||
|   center: [-122.44, 37.77], | ||||
|   zoom: 12, | ||||
| }); | ||||
| 
 | ||||
| ReactDOM.render(<Sidebar map={map} />, document.getElementById("sidebar")); | ||||
| 
 | ||||
| function formatHTMLforMarker(props) { | ||||
|   var { name, hours, address } = props; | ||||
|   var html = | ||||
|     '<div class="marker-title">' + | ||||
|     name + | ||||
|     "</div>" + | ||||
|     "<h4>Operating Hours</h4>" + | ||||
|     "<span>" + | ||||
|     hours + | ||||
|     "</span>" + | ||||
|     "<h4>Address</h4>" + | ||||
|     "<span>" + | ||||
|     address + | ||||
|     "</span>"; | ||||
|   return html; | ||||
| } | ||||
| 
 | ||||
| // setup popup display on the marker
 | ||||
| map.on("click", function (e) { | ||||
|   var features = map.queryRenderedFeatures( | ||||
|     e.point,  | ||||
|     { layers: ['trucks', 'trucks-highlight'], radius: 10, includeGeometry: true } | ||||
|   ); | ||||
| 
 | ||||
|   if (!features.length) return; | ||||
| 
 | ||||
|   var feature = features[0]; | ||||
| 
 | ||||
|   new mapboxgl.Popup() | ||||
|     .setLngLat(feature.geometry.coordinates) | ||||
|     .setHTML(formatHTMLforMarker(feature.properties)) | ||||
|     .addTo(map); | ||||
| }); | ||||
| @ -1,36 +0,0 @@ | ||||
| import React from "react"; | ||||
| 
 | ||||
| export default function Intro() { | ||||
|   return ( | ||||
|     <div className="intro"> | ||||
|       <h3>About</h3> | ||||
|       <p> | ||||
|         This is a fun application built to accompany the{" "} | ||||
|         <a href="http://prakhar.me/docker-curriculum">docker curriculum</a> - a | ||||
|         comprehensive tutorial on getting started with Docker targeted | ||||
|         especially at beginners. | ||||
|       </p> | ||||
|       <p> | ||||
|         The app is built with Flask on the backend and Elasticsearch is the | ||||
|         engine powering the search. | ||||
|       </p> | ||||
|       <p> | ||||
|         The frontend is hand-crafted with React and the beautiful maps are | ||||
|         courtesy of Mapbox. | ||||
|       </p> | ||||
|       <p> | ||||
|         If you find the design a bit ostentatious, blame{" "} | ||||
|         <a href="http://genius.com/Justin-bieber-baby-lyrics">Genius</a> for | ||||
|         giving me the idea of using this color scheme. If you love it, I smugly | ||||
|         take all the credit. ⊂(▀¯▀⊂) | ||||
|       </p> | ||||
|       <p> | ||||
|         Lastly, the data for the food trucks is made available in public domain | ||||
|         by{" "} | ||||
|         <a href="https://data.sfgov.org/Economy-and-Community/Mobile-Food-Facility-Permit/rqzj-sfat"> | ||||
|           SF Data | ||||
|         </a> | ||||
|       </p> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
| @ -1,205 +0,0 @@ | ||||
| import React from "react"; | ||||
| import request from "superagent"; | ||||
| import Intro from "./Intro"; | ||||
| import Vendor from "./Vendor"; | ||||
| 
 | ||||
| class Sidebar extends React.Component { | ||||
|   constructor(props) { | ||||
|     super(props); | ||||
|     this.state = { | ||||
|       results: [], | ||||
|       query: "", | ||||
|       firstLoad: true, | ||||
|     }; | ||||
|     this.onChange = this.onChange.bind(this); | ||||
|     this.handleSearch = this.handleSearch.bind(this); | ||||
|     this.handleHover = this.handleHover.bind(this); | ||||
|   } | ||||
| 
 | ||||
|   fetchResults() { | ||||
|     request.get("/search?q=" + this.state.query).end((err, res) => { | ||||
|       if (err) { | ||||
|         alert("error in fetching response"); | ||||
|       } else { | ||||
|         this.setState({ | ||||
|           results: res.body, | ||||
|           firstLoad: false, | ||||
|         }); | ||||
|         this.plotOnMap(); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   generateGeoJSON(markers) { | ||||
|     return { | ||||
|       type: "FeatureCollection", | ||||
|       features: markers.map((p) => ({ | ||||
|         type: "Feature", | ||||
|         properties: { | ||||
|           name: p.name, | ||||
|           hours: p.hours, | ||||
|           address: p.address, | ||||
|           "point-color": "253,237,57,1", | ||||
|         }, | ||||
|         geometry: { | ||||
|           type: "Point", | ||||
|           coordinates: [ | ||||
|             parseFloat(p.location.longitude), | ||||
|             parseFloat(p.location.latitude), | ||||
|           ], | ||||
|         }, | ||||
|       })), | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   plotOnMap(vendor) { | ||||
|     const map = this.props.map; | ||||
|     const results = this.state.results; | ||||
|     const markers = [].concat.apply( | ||||
|       [], | ||||
|       results.trucks.map((t) => | ||||
|         t.branches.map((b) => ({ | ||||
|           location: b.location, | ||||
|           name: t.name, | ||||
|           schedule: b.schedule, | ||||
|           hours: b.hours, | ||||
|           address: b.address, | ||||
|         })) | ||||
|       ) | ||||
|     ); | ||||
|     var highlightMarkers, usualMarkers, usualgeoJSON, highlightgeoJSON; | ||||
| 
 | ||||
|     if (vendor) { | ||||
|       highlightMarkers = markers.filter( | ||||
|         (m) => m.name.toLowerCase() === vendor.toLowerCase() | ||||
|       ); | ||||
|       usualMarkers = markers.filter( | ||||
|         (m) => m.name.toLowerCase() !== vendor.toLowerCase() | ||||
|       ); | ||||
|     } else { | ||||
|       usualMarkers = markers; | ||||
|     } | ||||
| 
 | ||||
|     usualgeoJSON = this.generateGeoJSON(usualMarkers); | ||||
|     if (highlightMarkers) { | ||||
|       highlightgeoJSON = this.generateGeoJSON(highlightMarkers); | ||||
|     } | ||||
|     // clearing layers
 | ||||
|     if (map.getLayer("trucks")) { | ||||
|       map.removeLayer("trucks"); | ||||
|     } | ||||
|     if (map.getSource("trucks")) { | ||||
|       map.removeSource("trucks"); | ||||
|     } | ||||
|     if (map.getLayer("trucks-highlight")) { | ||||
|       map.removeLayer("trucks-highlight"); | ||||
|     } | ||||
|     if (map.getSource("trucks-highlight")) { | ||||
|       map.removeSource("trucks-highlight"); | ||||
|     } | ||||
| 
 | ||||
|     map | ||||
|       .addSource("trucks", { | ||||
|         type: "geojson", | ||||
|         data: usualgeoJSON, | ||||
|       }) | ||||
|       .addLayer({ | ||||
|         id: "trucks", | ||||
|         type: "circle", | ||||
|         interactive: true, | ||||
|         source: "trucks", | ||||
|         paint: { | ||||
|           "circle-radius": 8, | ||||
|           "circle-color": "rgba(253,237,57,1)", | ||||
|         }, | ||||
|       }); | ||||
| 
 | ||||
|     if (highlightMarkers) { | ||||
|       map | ||||
|         .addSource("trucks-highlight", { | ||||
|           type: "geojson", | ||||
|           data: highlightgeoJSON, | ||||
|         }) | ||||
|         .addLayer({ | ||||
|           id: "trucks-highlight", | ||||
|           type: "circle", | ||||
|           interactive: true, | ||||
|           source: "trucks-highlight", | ||||
|           paint: { | ||||
|             "circle-radius": 8, | ||||
|             "circle-color": "rgba(164,65,99,1)", | ||||
|           }, | ||||
|         }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   handleSearch(e) { | ||||
|     e.preventDefault(); | ||||
|     this.fetchResults(); | ||||
|   } | ||||
| 
 | ||||
|   onChange(e) { | ||||
|     this.setState({ query: e.target.value }); | ||||
|   } | ||||
| 
 | ||||
|   handleHover(vendorName) { | ||||
|     this.plotOnMap(vendorName); | ||||
|   } | ||||
| 
 | ||||
|   render() { | ||||
|     if (this.state.firstLoad) { | ||||
|       return ( | ||||
|         <div> | ||||
|           <div id="search-area"> | ||||
|             <form onSubmit={this.handleSearch}> | ||||
|               <input | ||||
|                 type="text" | ||||
|                 value={this.state.query} | ||||
|                 onChange={this.onChange} | ||||
|                 placeholder="Burgers, Tacos or Wraps?" | ||||
|               /> | ||||
|               <button>Search!</button> | ||||
|             </form> | ||||
|           </div> | ||||
|           <Intro /> | ||||
|         </div> | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     const query = this.state.query; | ||||
|     const resultsCount = this.state.results.hits || 0; | ||||
|     const locationsCount = this.state.results.locations || 0; | ||||
|     const results = this.state.results.trucks || []; | ||||
|     const renderedResults = results.map((r, i) => ( | ||||
|       <Vendor key={i} data={r} handleHover={this.handleHover} /> | ||||
|     )); | ||||
| 
 | ||||
|     return ( | ||||
|       <div> | ||||
|         <div id="search-area"> | ||||
|           <form onSubmit={this.handleSearch}> | ||||
|             <input | ||||
|               type="text" | ||||
|               value={query} | ||||
|               onChange={this.onChange} | ||||
|               placeholder="Burgers, Tacos or Wraps?" | ||||
|             /> | ||||
|             <button>Search!</button> | ||||
|           </form> | ||||
|         </div> | ||||
|         {resultsCount > 0 ? ( | ||||
|           <div id="results-area"> | ||||
|             <h5> | ||||
|               Found <span className="highlight">{resultsCount}</span> vendors in{" "} | ||||
|               <span className="highlight">{locationsCount}</span> different | ||||
|               locations | ||||
|             </h5> | ||||
|             <ul> {renderedResults} </ul> | ||||
|           </div> | ||||
|         ) : null} | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export default Sidebar; | ||||
| @ -1,70 +0,0 @@ | ||||
| import React from "react"; | ||||
| 
 | ||||
| export default class Vendor extends React.Component { | ||||
|   constructor(props) { | ||||
|     super(props); | ||||
|     this.state = { | ||||
|       isExpanded: false, | ||||
|     }; | ||||
| 
 | ||||
|     this.toggleExpand = this.toggleExpand.bind(this); | ||||
|   } | ||||
| 
 | ||||
|   formatFoodItems(items) { | ||||
|     if (this.state.isExpanded) { | ||||
|       return items.join(", "); | ||||
|     } | ||||
|     const summary = items.join(", ").substr(0, 80); | ||||
|     if (summary.length > 70) { | ||||
|       const indexOfLastSpace = | ||||
|         summary.split("").reverse().join("").indexOf(",") + 1; | ||||
|       return summary.substr(0, 80 - indexOfLastSpace) + " & more..."; | ||||
|     } | ||||
|     return summary; | ||||
|   } | ||||
| 
 | ||||
|   toggleExpand() { | ||||
|     this.setState({ | ||||
|       isExpanded: !this.state.isExpanded, | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   render() { | ||||
|     const { name, branches, fooditems, drinks } = this.props.data; | ||||
|     const servesDrinks = ( | ||||
|       <div className="row"> | ||||
|         <div className="icons"> | ||||
|           {" "} | ||||
|           <i className="ion-wineglass"></i>{" "} | ||||
|         </div> | ||||
|         <div className="content">Serves Cold Drinks</div> | ||||
|       </div> | ||||
|     ); | ||||
| 
 | ||||
|     return ( | ||||
|       <li | ||||
|         onMouseEnter={this.props.handleHover.bind(null, name)} | ||||
|         onClick={this.toggleExpand} | ||||
|       > | ||||
|         <p className="truck-name">{name}</p> | ||||
|         <div className="row"> | ||||
|           <div className="icons"> | ||||
|             {" "} | ||||
|             <i className="ion-android-pin"></i>{" "} | ||||
|           </div> | ||||
|           <div className="content"> {branches.length} locations </div> | ||||
|         </div> | ||||
|         {drinks ? servesDrinks : null} | ||||
|         <div className="row"> | ||||
|           <div className="icons"> | ||||
|             {" "} | ||||
|             <i className="ion-fork"></i> <i className="ion-spoon"></i> | ||||
|           </div> | ||||
|           <div className="content"> | ||||
|             Serves {this.formatFoodItems(fooditems)} | ||||
|           </div> | ||||
|         </div> | ||||
|       </li> | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @ -1,202 +0,0 @@ | ||||
| html, | ||||
| body { | ||||
|   padding: 0; | ||||
|   color: #aaa; | ||||
|   box-sizing: border-box; | ||||
|   font-family: "Titillium Web", sans-serif; | ||||
|   margin: 0; | ||||
| } | ||||
| 
 | ||||
| .github-corner:hover .octo-arm { | ||||
|   animation: octocat-wave 560ms ease-in-out; | ||||
| } | ||||
| @keyframes octocat-wave { | ||||
|   0%, | ||||
|   100% { | ||||
|     transform: rotate(0); | ||||
|   } | ||||
|   20%, | ||||
|   60% { | ||||
|     transform: rotate(-25deg); | ||||
|   } | ||||
|   40%, | ||||
|   80% { | ||||
|     transform: rotate(10deg); | ||||
|   } | ||||
| } | ||||
| @media (max-width: 500px) { | ||||
|   .github-corner:hover .octo-arm { | ||||
|     animation: none; | ||||
|   } | ||||
|   .github-corner .octo-arm { | ||||
|     animation: octocat-wave 560ms ease-in-out; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| i { | ||||
|   color: #3a3a3a; | ||||
| } | ||||
| 
 | ||||
| div.intro a, | ||||
| div.intro a:visited { | ||||
|   color: #fded39; | ||||
| } | ||||
| div.intro { | ||||
|   padding: 10px; | ||||
| } | ||||
| 
 | ||||
| div.intro h3 { | ||||
|   color: #fded39; | ||||
|   text-transform: uppercase; | ||||
| } | ||||
| 
 | ||||
| h1 i { | ||||
|   color: black; | ||||
|   text-shadow: none; | ||||
|   font-size: 21px; | ||||
| } | ||||
| textarea, | ||||
| input, | ||||
| button { | ||||
|   outline: none; | ||||
| } | ||||
| 
 | ||||
| .container { | ||||
|   height: 90vh; | ||||
|   display: flex; | ||||
| } | ||||
| 
 | ||||
| #map { | ||||
|   flex: 3; | ||||
| } | ||||
| 
 | ||||
| #sidebar { | ||||
|   background: #1a1a1a; | ||||
|   border-left: 1px solid #444444; | ||||
|   width: 320px; | ||||
|   flex: 1; | ||||
|   overflow-y: scroll; | ||||
| } | ||||
| 
 | ||||
| #sidebar div#results-area { | ||||
|   overflow-y: scroll; | ||||
| } | ||||
| 
 | ||||
| div#heading { | ||||
|   background: #fded39; | ||||
|   margin: 0; | ||||
|   height: 10vh; | ||||
|   text-align: center; | ||||
| } | ||||
| 
 | ||||
| div#heading h1 { | ||||
|   font-size: 25px; | ||||
|   text-transform: uppercase; | ||||
|   text-shadow: -2px 2px black; | ||||
|   margin: 0; | ||||
|   color: #fded39; | ||||
| } | ||||
| 
 | ||||
| div#heading p { | ||||
|   color: black; | ||||
|   font-style: italic; | ||||
|   font-size: 12px; | ||||
|   margin: 0; | ||||
| } | ||||
| 
 | ||||
| div#search-area { | ||||
|   padding: 10px; | ||||
|   border-bottom: 1px solid #444444; | ||||
| } | ||||
| 
 | ||||
| div#search-area input, | ||||
| div#search-area button { | ||||
|   padding: 10px 8px; | ||||
|   border: none; | ||||
| } | ||||
| div#search-area input { | ||||
|   width: 195px; | ||||
| } | ||||
| div#search-area button { | ||||
|   background: #fded39; | ||||
|   font-weight: 600; | ||||
|   text-transform: uppercase; | ||||
| } | ||||
| 
 | ||||
| div#results-area { | ||||
|   padding: 10px; | ||||
| } | ||||
| div#results-area h5 { | ||||
|   font-weight: 200; | ||||
|   font-style: italic; | ||||
|   margin: 0; | ||||
|   color: #ddd; | ||||
| } | ||||
| div#results-area h5 span.highlight { | ||||
|   color: #fded39; | ||||
|   font-weight: 600; | ||||
| } | ||||
| div#results-area ul { | ||||
|   padding: 0; | ||||
|   list-style-type: none; | ||||
| } | ||||
| 
 | ||||
| div#results-area ul li { | ||||
|   border: 1px solid #444; | ||||
|   padding: 5px 10px; | ||||
|   margin-bottom: 10px; | ||||
|   cursor: pointer; | ||||
| } | ||||
| 
 | ||||
| div#results-area ul li:hover { | ||||
|   border: 1px solid #fded39; | ||||
| } | ||||
| 
 | ||||
| div#results-area ul li p { | ||||
|   margin: 0; | ||||
|   font-size: 14px; | ||||
| } | ||||
| 
 | ||||
| div#results-area ul li p.truck-name { | ||||
|   color: #fded39; | ||||
|   text-transform: uppercase; | ||||
|   margin: 0; | ||||
|   font-size: 16px; | ||||
|   margin-bottom: 5px; | ||||
| } | ||||
| div#results-area ul li div.row { | ||||
|   display: flex; | ||||
|   flex-direction: row; | ||||
| } | ||||
| div#results-area ul li div.row div.icons { | ||||
|   flex-shrink: 0; | ||||
|   width: 16px; | ||||
| } | ||||
| 
 | ||||
| div#results-area ul li div.row div.content { | ||||
|   margin-left: 8px; | ||||
| } | ||||
| 
 | ||||
| .mapboxgl-popup-content { | ||||
|   background: black; | ||||
|   font-family: "Titillium Web", sans-serif; | ||||
| } | ||||
| 
 | ||||
| .mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip { | ||||
|   border-top-color: black; | ||||
| } | ||||
| .mapboxgl-popup-anchor-top .mapboxgl-popup-tip { | ||||
|   border-bottom-color: black; | ||||
| } | ||||
| .mapboxgl-popup-close-button { | ||||
|   color: white; | ||||
| } | ||||
| .mapboxgl-popup-content .marker-title { | ||||
|   color: #fded39; | ||||
|   text-transform: uppercase; | ||||
|   font-size: 14px; | ||||
| } | ||||
| .mapboxgl-popup-content h4 { | ||||
|   margin: 0; | ||||
|   margin-top: 10px; | ||||
| } | ||||
| @ -1,50 +0,0 @@ | ||||
| <!DOCTYPE html> | ||||
| <html> | ||||
| <head> | ||||
|     <meta charset='utf-8' /> | ||||
|     <title>SF Food Trucks</title> | ||||
|     <meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' /> | ||||
|     <link href='https://fonts.googleapis.com/css?family=Titillium+Web:400,700' rel='stylesheet' type='text/css'> | ||||
|     <script src='https://api.mapbox.com/mapbox-gl-js/v1.9.1/mapbox-gl.js'></script> | ||||
|     <link href='https://api.mapbox.com/mapbox-gl-js/v1.9.1/mapbox-gl.css' rel='stylesheet' /> | ||||
|     <link href='/static/styles/main.css' rel='stylesheet' /> | ||||
|     <link rel="stylesheet" href="http://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css" /> | ||||
|     <link rel="apple-touch-icon" sizes="57x57" href="/static/icons/apple-icon-57x57.png"> | ||||
|     <link rel="apple-touch-icon" sizes="60x60" href="/static/icons/apple-icon-60x60.png"> | ||||
|     <link rel="apple-touch-icon" sizes="72x72" href="/static/icons/apple-icon-72x72.png"> | ||||
|     <link rel="apple-touch-icon" sizes="76x76" href="/static/icons/apple-icon-76x76.png"> | ||||
|     <link rel="apple-touch-icon" sizes="114x114" href="/static/icons/apple-icon-114x114.png"> | ||||
|     <link rel="apple-touch-icon" sizes="120x120" href="/static/icons/apple-icon-120x120.png"> | ||||
|     <link rel="apple-touch-icon" sizes="144x144" href="/static/icons/apple-icon-144x144.png"> | ||||
|     <link rel="apple-touch-icon" sizes="152x152" href="/static/icons/apple-icon-152x152.png"> | ||||
|     <link rel="apple-touch-icon" sizes="180x180" href="/static/icons/apple-icon-180x180.png"> | ||||
|     <link rel="icon" type="image/png" sizes="192x192"  href="/static/icons//android-icon-192x192.png"> | ||||
|     <link rel="icon" type="image/png" sizes="32x32" href="/static/icons/favicon-32x32.png"> | ||||
|     <link rel="icon" type="image/png" sizes="96x96" href="/static/icons/favicon-96x96.png"> | ||||
|     <link rel="icon" type="image/png" sizes="16x16" href="/static/icons/favicon-16x16.png"> | ||||
|     <meta name="msapplication-TileColor" content="#ffffff"> | ||||
|     <meta name="msapplication-TileImage" content="/ms-icon-144x144.png"> | ||||
|     <meta name="theme-color" content="#ffffff"> | ||||
| 
 | ||||
| </head> | ||||
| <body> | ||||
| 
 | ||||
|   <!-- awesome svg octocat thanks to http://tholman.com/ --> | ||||
|   <a href="https://github.com/prakhar1989/FoodTrucks/" class="github-corner" title="Fork me on Github"> | ||||
|     <svg width="72" height="72" viewBox="0 0 250 250" style="fill:#000; color:#FDED39; position: absolute; top: 0; border: 0; left: 0; transform: scale(-1, 1);"> | ||||
|       <path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path> | ||||
|       <path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path> | ||||
|       <path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path> | ||||
|     </svg> | ||||
|   </a> | ||||
|   <div id="heading"> | ||||
|     <h1>SF F <i class="ion-pizza"></i> <i class="ion-icecream"></i> d Trucks</h1> | ||||
|     <p>San Francisco's finger-licking street food now at your fingertips.</p> | ||||
|   </div> | ||||
|   <div class="container"> | ||||
|     <div id='map'></div> | ||||
|     <div id="sidebar"> </div> | ||||
|   </div> | ||||
|   <script src='/static/build/main.js'></script> | ||||
| </body> | ||||
| </html> | ||||
| @ -1,19 +0,0 @@ | ||||
| module.exports = { | ||||
|     cache: true, | ||||
|     entry: './static/src/app.js', | ||||
|     output: { | ||||
|         filename: './static/build/main.js' | ||||
|     }, | ||||
|     devtool: 'source-map', | ||||
|     module: { | ||||
|         loaders: [ | ||||
|         { | ||||
|             test: /\.js$/, | ||||
|             loader: 'babel-loader', | ||||
|             query: { | ||||
|                 presets: ['es2015', 'react'] | ||||
|             } | ||||
|         }, | ||||
|         ] | ||||
|     } | ||||
| }; | ||||
| @ -1,46 +0,0 @@ | ||||
| import json | ||||
| import requests | ||||
| 
 | ||||
| def getData(url): | ||||
|     r = requests.get(url) | ||||
|     return r.json() | ||||
| 
 | ||||
| def convertData(data, msymbol="restaurant", msize="medium"): | ||||
|     data_dict = [] | ||||
|     for d in data: | ||||
|         if d.get('longitude') and d.get("latitude"): | ||||
|             data_dict.append({ | ||||
|                 "type": "Feature", | ||||
|                 "geometry": { | ||||
|                     "type": "Point", | ||||
|                     "coordinates": [float(d["longitude"]), | ||||
|                                     float(d["latitude"])] | ||||
|                 }, | ||||
|                 "properties": { | ||||
|                     "name": d.get("applicant", ""), | ||||
|                     "marker-symbol": msymbol, | ||||
|                     "marker-size": msize, | ||||
|                     "marker-color": "#CC0033", | ||||
|                     "fooditems": d.get('fooditems', ""), | ||||
|                     "address": d.get("address", "") | ||||
|                 } | ||||
|             }) | ||||
|     return data_dict | ||||
| 
 | ||||
| def writeToFile(data, filename="data.geojson"): | ||||
|     template = { | ||||
|                 "type": "FeatureCollection", | ||||
|                 "crs": { | ||||
|                     "type": "name", | ||||
|                     "properties": { | ||||
|                       "name": "urn:ogc:def:crs:OGC:1.3:CRS84" | ||||
|                     }, | ||||
|                 }, | ||||
|                 "features": data } | ||||
|     with open(filename, "w") as f: | ||||
|         json.dump(template, f, indent=2) | ||||
|     print "Geojson generated" | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
|     data = getData("http://data.sfgov.org/resource/rqzj-sfat.json") | ||||
|     writeToFile(convertData(data[:350]), filename="trucks.geojson") | ||||