diff --git a/.gitignore b/.gitignore index 1df2d7f..78fc94d 100644 --- a/.gitignore +++ b/.gitignore @@ -36,5 +36,14 @@ terraform.tfstate.d/ .terraform/ .terraform.lock.hcl terraform.tfvars +terraform.tfstate* .venv/ + +# Lambda Python Stuff +serverless/*/lambda.zip +serverless/*/src/* +!serverless/*/src/alpr_counts.py +!serverless/*/src/requirements.txt + +# TODO: need a better way to handle python packages diff --git a/serverless/alpr-counts/src/alpr_counts.py b/serverless/alpr-counts/src/alpr_counts.py new file mode 100644 index 0000000..c357a37 --- /dev/null +++ b/serverless/alpr-counts/src/alpr_counts.py @@ -0,0 +1,48 @@ +import json +import requests +import boto3 + +def fetch_alpr_surveillance_nodes(usOnly=False): + overpass_url = "http://overpass-api.de/api/interpreter" + overpass_query = f""" + [out:json]; + {'area["ISO3166-1"="US"]->.searchArea;' if usOnly else ''} + node["man_made"="surveillance"]["surveillance:type"="ALPR"]{f'(area.searchArea)' if usOnly else ''}; + out count; + """ + + response = requests.get(overpass_url, params={'data': overpass_query}) + + if response.status_code == 200: + response_json = response.json() + try: + return response_json['elements'][0]['tags']['nodes'] + except (IndexError, KeyError) as e: + return {"error": "Could not find 'elements[0].tags.nodes' in the response."} + else: + return {"error": f"Failed to fetch data from Overpass API. Status code: {response.status_code}"} + +def lambda_handler(event, context): + us_alprs = fetch_alpr_surveillance_nodes('(area["ISO3166-1"="US"])') + worldwide_alprs = fetch_alpr_surveillance_nodes() + + all_alprs = { + 'us': us_alprs, + 'worldwide': worldwide_alprs + } + + s3 = boto3.client('s3') + bucket = 'deflock-clusters' + key = 'alpr-counts.json' + + s3.put_object( + Bucket=bucket, + Key=key, + Body=json.dumps(all_alprs), + ContentType='application/json' + ) + + return { + 'statusCode': 200, + 'body': 'Successfully fetched ALPR counts.', + } diff --git a/serverless/alpr-counts/src/requirements.txt b/serverless/alpr-counts/src/requirements.txt new file mode 100644 index 0000000..a873d92 --- /dev/null +++ b/serverless/alpr-counts/src/requirements.txt @@ -0,0 +1,2 @@ +requests +boto3 diff --git a/terraform/alpr-counts.tf b/terraform/alpr-counts.tf new file mode 100644 index 0000000..5d0d136 --- /dev/null +++ b/terraform/alpr-counts.tf @@ -0,0 +1,101 @@ +locals { + alpr_counts_filename = "alpr-counts.json" +} + + +provider "aws" { + region = "us-east-1" +} + +resource "aws_iam_role" "lambda_role" { + name = "lambda_s3_write_role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "lambda.amazonaws.com" + } + } + ] + }) +} + +resource "aws_iam_policy" "lambda_s3_write_policy" { + name = "lambda_s3_write_policy" + description = "Policy for Lambda to write to S3 bucket deflock-clusters" + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = [ + "s3:PutObject", + "s3:PutObjectAcl" + ] + Effect = "Allow" + Resource = "arn:aws:s3:::deflock-clusters/${local.alpr_counts_filename}" + } + ] + }) +} + +resource "aws_iam_role_policy_attachment" "lambda_s3_write_attachment" { + role = aws_iam_role.lambda_role.name + policy_arn = aws_iam_policy.lambda_s3_write_policy.arn +} + +resource "null_resource" "pip_install" { + provisioner "local-exec" { + command = < { - name = dvo.resource_record_name - type = dvo.resource_record_type - value = dvo.resource_record_value - zone_id = aws_route53_zone.deflock_me.zone_id - } - } - - zone_id = each.value.zone_id - name = each.value.name - type = each.value.type - ttl = 60 - records = [each.value.value] -} - -# ACM Certificate Validation Completion -resource "aws_acm_certificate_validation" "deflock_me_cert_validation" { - certificate_arn = aws_acm_certificate.deflock_me_cert.arn - validation_record_fqdns = [for record in aws_route53_record.deflock_me_cert_validation : record.fqdn] -} - -# CloudFront Distribution for CDN and HTTPS -resource "aws_cloudfront_distribution" "vue_app_cdn" { - origin { - domain_name = aws_s3_bucket.vue_app.website_endpoint - origin_id = "S3-VueApp" - - custom_origin_config { - http_port = 80 - https_port = 443 - origin_protocol_policy = "http-only" - origin_ssl_protocols = ["TLSv1.2"] - } - } - - enabled = true - is_ipv6_enabled = true - comment = "CDN for ${var.domain_name}" - default_root_object = "index.html" - - aliases = ["${var.domain_name}", "www.${var.domain_name}"] - - default_cache_behavior { - allowed_methods = ["GET", "HEAD"] - cached_methods = ["GET", "HEAD"] - target_origin_id = "S3-VueApp" - - forwarded_values { - query_string = false - cookies { - forward = "none" - } - } - - viewer_protocol_policy = "redirect-to-https" - min_ttl = 0 - default_ttl = 3600 - max_ttl = 86400 - } - - restrictions { - geo_restriction { - restriction_type = "none" - } - } - - viewer_certificate { - acm_certificate_arn = aws_acm_certificate_validation.deflock_me_cert_validation.certificate_arn - ssl_support_method = "sni-only" - minimum_protocol_version = "TLSv1.2_2021" - } - - tags = { - Name = "CloudFront Vue App CDN" - } -} - -# Route 53 Records for Domain -resource "aws_route53_record" "deflock_me_root" { - zone_id = aws_route53_zone.deflock_me.zone_id - name = var.domain_name - type = "A" - - alias { - name = aws_cloudfront_distribution.vue_app_cdn.domain_name - zone_id = aws_cloudfront_distribution.vue_app_cdn.hosted_zone_id - evaluate_target_health = false - } -} - -resource "aws_route53_record" "deflock_me_www" { - zone_id = aws_route53_zone.deflock_me.zone_id - name = "www.${var.domain_name}" - type = "A" - - alias { - name = aws_cloudfront_distribution.vue_app_cdn.domain_name - zone_id = aws_cloudfront_distribution.vue_app_cdn.hosted_zone_id - evaluate_target_health = false - } -} diff --git a/webapp/src/components/ALPRCounter.vue b/webapp/src/components/ALPRCounter.vue new file mode 100644 index 0000000..dbf8e6b --- /dev/null +++ b/webapp/src/components/ALPRCounter.vue @@ -0,0 +1,44 @@ + + + diff --git a/webapp/src/services/apiService.ts b/webapp/src/services/apiService.ts index 42460a3..88b45db 100644 --- a/webapp/src/services/apiService.ts +++ b/webapp/src/services/apiService.ts @@ -65,6 +65,12 @@ export const getALPRs = async (boundingBox: BoundingBox) => { return response.data; } +export const getALPRCounts = async () => { + const s3Url = "https://deflock-clusters.s3.us-east-1.amazonaws.com/alpr-counts.json"; + const response = await apiService.get(s3Url); + return response.data; +} + export const getClusters = async () => { const s3Url = "https://deflock-clusters.s3.us-east-1.amazonaws.com/alpr_clusters.json"; const response = await apiService.get(s3Url); diff --git a/webapp/src/views/HomeView.vue b/webapp/src/views/HomeView.vue index 5f13a02..a936f5a 100644 --- a/webapp/src/views/HomeView.vue +++ b/webapp/src/views/HomeView.vue @@ -17,6 +17,10 @@ @ready="mapLoaded" :options="{ zoomControl: false, attributionControl: false }" > + + + +