From af91ab3a9c79ce1ec3d80a4043beaaef9c9fa978 Mon Sep 17 00:00:00 2001 From: Will Freeman Date: Sun, 1 Jun 2025 19:04:31 -0600 Subject: [PATCH] update popup, update pages --- .../src/main/scala/models/OperatorInfo.scala | 16 +++ .../main/scala/services/DynamoDBClient.scala | 77 +++++++++++ webapp/src/components/DFMapPopup.vue | 128 +++++++++--------- webapp/src/components/layout/Footer.vue | 2 +- webapp/src/views/FOIA.vue | 28 ++-- 5 files changed, 175 insertions(+), 76 deletions(-) create mode 100644 shotgun/src/main/scala/models/OperatorInfo.scala create mode 100644 shotgun/src/main/scala/services/DynamoDBClient.scala diff --git a/shotgun/src/main/scala/models/OperatorInfo.scala b/shotgun/src/main/scala/models/OperatorInfo.scala new file mode 100644 index 0000000..d104003 --- /dev/null +++ b/shotgun/src/main/scala/models/OperatorInfo.scala @@ -0,0 +1,16 @@ +package models +import software.amazon.awssdk.services.dynamodb.model.AttributeValue +import spray.json._ + +case class OperatorInfo(wikidataId: String, transparencyPortalUrl: String) + +object OperatorInfoJsonProtocol extends DefaultJsonProtocol { + implicit val operatorInfoFormat: RootJsonFormat[OperatorInfo] = jsonFormat2(OperatorInfo) + + def fromAttributeValueMap(map: Map[String, AttributeValue]): Option[OperatorInfo] = { + for { + wikidataId <- map.get("wikidataId").flatMap(attr => Option(attr.s())) + transparencyPortalUrl <- map.get("transparencyPortalUrl").flatMap(attr => Option(attr.s())) + } yield OperatorInfo(wikidataId, transparencyPortalUrl) + } +} diff --git a/shotgun/src/main/scala/services/DynamoDBClient.scala b/shotgun/src/main/scala/services/DynamoDBClient.scala new file mode 100644 index 0000000..85ab460 --- /dev/null +++ b/shotgun/src/main/scala/services/DynamoDBClient.scala @@ -0,0 +1,77 @@ +package services + +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider +import software.amazon.awssdk.regions.Region +import software.amazon.awssdk.services.dynamodb.DynamoDbClient +import software.amazon.awssdk.services.dynamodb.model._ + +import scala.concurrent.{ExecutionContext, Future} +import scala.collection.JavaConverters._ +import scala.util.Try + +class DynamoDBClient(region: Region = Region.US_EAST_1)(implicit ec: ExecutionContext) { + + // Create the base DynamoDB client + private val dynamoDbClient = DynamoDbClient.builder() + .region(region) + .credentialsProvider(DefaultCredentialsProvider.create()) + .build() + + // Get an item using the standard client + private def getItem(tableName: String, key: java.util.Map[String, AttributeValue]): Future[Option[java.util.Map[String, AttributeValue]]] = Future { + val request = GetItemRequest.builder() + .tableName(tableName) + .key(key) + .build() + + dynamoDbClient.getItem(request).item() match { + case item if item.isEmpty => None + case item => Some(item) + } + } + + // Get item with Scala Map instead of Java Map + def getItem(tableName: String, key: Map[String, AttributeValue]): Future[Option[Map[String, AttributeValue]]] = { + getItem(tableName, key.asJava).map { + _.map(_.asScala.toMap) + } + } + + // Put an item + def putItem(tableName: String, item: Map[String, AttributeValue]): Future[PutItemResponse] = Future { + val request = PutItemRequest.builder() + .tableName(tableName) + .item(item.asJava) + .build() + + dynamoDbClient.putItem(request) + } + + // Delete an item + def deleteItem(tableName: String, key: Map[String, AttributeValue]): Future[DeleteItemResponse] = Future { + val request = DeleteItemRequest.builder() + .tableName(tableName) + .key(key.asJava) + .build() + + dynamoDbClient.deleteItem(request) + } + + // Scan items + def scanItems(tableName: String): Future[List[Map[String, AttributeValue]]] = Future { + val request = ScanRequest.builder() + .tableName(tableName) + .build() + + dynamoDbClient.scan(request) + .items() + .asScala + .map(_.asScala.toMap) + .toList + } + + // Close resources + def close(): Unit = { + dynamoDbClient.close() + } +} diff --git a/webapp/src/components/DFMapPopup.vue b/webapp/src/components/DFMapPopup.vue index 803cf7b..1e15154 100644 --- a/webapp/src/components/DFMapPopup.vue +++ b/webapp/src/components/DFMapPopup.vue @@ -1,56 +1,51 @@ @@ -59,7 +54,7 @@ import { computed } from 'vue'; import type { PropType } from 'vue'; import type { ALPR } from '@/types'; -import { VIcon, VList, VSheet, VListItem, VBtn } from 'vuetify/components'; +import { VIcon, VList, VSheet, VListItem, VBtn, VImg, VListItemSubtitle, VDivider } from 'vuetify/components'; const props = defineProps({ alpr: { @@ -68,24 +63,35 @@ const props = defineProps({ } }); -const isFaceRecognition = computed(() => props.alpr.tags.brand === 'Avigilon'); +const manufacturer = computed(() => ( + props.alpr.tags.manufacturer || props.alpr.tags.brand || 'Unknown' +)); -const cardinalDirection = computed(() => { - const direction = props.alpr.tags.direction || props.alpr.tags["camera:direction"]; - if (direction === undefined) { - return 'Unspecified Direction'; - } else if (direction.includes(';')) { - return 'Faces Multiple Directions'; - } else { - return /^\d+$/.test(direction) ? degreesToCardinal(parseInt(direction)) : direction; +const transparencyLink = computed(() => { + // XXX: eventually get this from /api/operator-info?wikidata=Q1234 + return `https://transparency.flocksafety.com/boulder-co-pd`; +}); + +const abbreviatedOperator = computed(() => { + if (props.alpr.tags.operator === undefined) { + return 'Unknown'; } -} -); -function degreesToCardinal(degrees: number): string { - const cardinals = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NE']; - return 'Faces ' + cardinals[Math.round(degrees / 45) % 8]; -} + const replacements: Record = { + "Police Department": "PD", + "Sheriff's Office": "SO", + "Sheriffs Office": "SO", + // TODO: maybe include HOAs + }; + + const operator = props.alpr.tags.operator; + for (const [full, abbr] of Object.entries(replacements)) { + if (operator.includes(full)) { + return operator.replace(full, abbr); + } + } + return operator; +}); function osmNodeLink(id: string): string { return `https://www.openstreetmap.org/node/${id}`; diff --git a/webapp/src/components/layout/Footer.vue b/webapp/src/components/layout/Footer.vue index 73d342c..81e55ba 100644 --- a/webapp/src/components/layout/Footer.vue +++ b/webapp/src/components/layout/Footer.vue @@ -74,7 +74,7 @@ const currentYear = new Date().getFullYear(); const internalLinks = [ { title: 'About', to: '/about', icon: 'mdi-information' }, - { title: 'Privacy Policy', to: '/privacy', icon: 'mdi-shield' }, + { title: 'Privacy Policy', to: '/privacy', icon: 'mdi-shield-lock' }, { title: 'Terms of Service', to: '/terms', icon: 'mdi-file-document' }, { title: 'Contact', to: '/contact', icon: 'mdi-email' }, ]; diff --git a/webapp/src/views/FOIA.vue b/webapp/src/views/FOIA.vue index 99bc13c..dbbb07c 100644 --- a/webapp/src/views/FOIA.vue +++ b/webapp/src/views/FOIA.vue @@ -1,33 +1,33 @@