mirror of
https://github.com/FoggedLens/deflock.git
synced 2026-02-12 15:02:45 +00:00
update popup, update pages
This commit is contained in:
16
shotgun/src/main/scala/models/OperatorInfo.scala
Normal file
16
shotgun/src/main/scala/models/OperatorInfo.scala
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
77
shotgun/src/main/scala/services/DynamoDBClient.scala
Normal file
77
shotgun/src/main/scala/services/DynamoDBClient.scala
Normal file
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -1,56 +1,51 @@
|
||||
<template>
|
||||
<v-sheet min-width="240">
|
||||
<v-list density="compact">
|
||||
<v-list-item v-if="isFaceRecognition">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon start>mdi-face-recognition</v-icon> <b>Face Recognition</b>
|
||||
</div>
|
||||
</v-list-item>
|
||||
<v-sheet width="220">
|
||||
<!-- TODO: if a field is unknown, prompt user to edit it -->
|
||||
<v-img cover width="100%" height="120px" src="/alprs/flock-3.jpg" />
|
||||
<v-list density="compact" class="my-2">
|
||||
<v-list-item>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon start>mdi-cctv</v-icon> <b>License Plate Reader</b>
|
||||
</div>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="isFaceRecognition">
|
||||
<v-icon start>mdi-adjust</v-icon> <b>Omnidirectional</b>
|
||||
</v-list-item>
|
||||
<v-list-item v-else>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon start>mdi-compass-outline</v-icon> <b>{{ cardinalDirection }}</b>
|
||||
</div>
|
||||
<template v-slot:prepend>
|
||||
<v-icon icon="mdi-police-badge"></v-icon>
|
||||
</template>
|
||||
|
||||
<v-list-item-subtitle style="font-size: 1em">
|
||||
Operated by
|
||||
</v-list-item-subtitle>
|
||||
|
||||
<b>
|
||||
<span style="font-size: 1.25em">
|
||||
{{ abbreviatedOperator }}
|
||||
</span>
|
||||
</b>
|
||||
|
||||
<template v-slot:append>
|
||||
<v-btn icon size="small" variant="text" :href="transparencyLink" target="_blank" color="primary">
|
||||
<v-icon icon="mdi-open-in-new"></v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<v-divider class="my-2" />
|
||||
|
||||
<v-list-item>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon start>mdi-domain</v-icon> <b>
|
||||
<span v-if="alpr.tags.manufacturer">
|
||||
{{ alpr.tags.manufacturer }}
|
||||
</span>
|
||||
<span v-else-if="alpr.tags.brand">
|
||||
{{ alpr.tags.brand }}
|
||||
</span>
|
||||
<span v-else>
|
||||
Unspecified Manufacturer
|
||||
</span>
|
||||
</b>
|
||||
</div>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon start>mdi-account-tie</v-icon>
|
||||
<b>
|
||||
<span v-if="alpr.tags.operator">
|
||||
{{ alpr.tags.operator }}
|
||||
</span>
|
||||
<span v-else>
|
||||
Unspecified Operator
|
||||
</span>
|
||||
</b>
|
||||
</div>
|
||||
<template v-slot:prepend>
|
||||
<v-icon icon="mdi-factory"></v-icon>
|
||||
</template>
|
||||
|
||||
<v-list-item-subtitle style="font-size: 1em">
|
||||
Made by
|
||||
</v-list-item-subtitle>
|
||||
|
||||
<b>
|
||||
<span style="font-size: 1.25em">
|
||||
{{ manufacturer }}
|
||||
</span>
|
||||
</b>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<div class="text-center">
|
||||
<v-btn target="_blank" size="x-small" :href="osmNodeLink(props.alpr.id)" variant="text" color="grey-darken-1"><v-icon start>mdi-open-in-new</v-icon>View on OSM</v-btn>
|
||||
<v-btn target="_blank" size="x-small" :href="osmNodeLink(props.alpr.id)" variant="text" color="grey"><v-icon start>mdi-open-in-new</v-icon>View on OSM</v-btn>
|
||||
</div>
|
||||
</v-sheet>
|
||||
</template>
|
||||
@@ -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<string, string> = {
|
||||
"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}`;
|
||||
|
||||
@@ -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' },
|
||||
];
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
<template>
|
||||
<Hero
|
||||
imageUrl="/documents.jpg"
|
||||
title="How to FOIA"
|
||||
title="How to Request Public Records"
|
||||
description="Learn how simple it is to request ALPR data from your city"
|
||||
button-text="Get Started"
|
||||
button-href="#how-to"
|
||||
/>
|
||||
|
||||
<v-container class="mb-12">
|
||||
<h2>Why File a FOIA Request</h2>
|
||||
<h2>Why File a Public Records Request</h2>
|
||||
<p>
|
||||
ALPRs are being installed across the country with little transparency. Public records requests allow us to track where these cameras are, ensuring that all installations are documented. By obtaining invoices, we can verify the number of cameras deployed in your area, and our mappers will work to add them all to the map.
|
||||
</p>
|
||||
<p>
|
||||
Your help is crucial. Filing a FOIA request is easy, and your contribution will help us build a complete map of these surveillance networks. We'll even give you a <a href="#how-to">template to use</a>.
|
||||
Your help is crucial. Filing a public records request is easy, and your contribution will help us build a complete map of these surveillance networks. We'll even give you a <a href="#how-to">template to use</a>.
|
||||
</p>
|
||||
|
||||
<v-divider class="my-8" />
|
||||
|
||||
<h2>FOIA Basics</h2>
|
||||
<h2>Public Records Basics</h2>
|
||||
|
||||
<h3>What is a FOIA request?</h3>
|
||||
<h3>What is a public records request?</h3>
|
||||
<p>
|
||||
A Freedom of Information Act (FOIA) request is a formal way to ask government agencies for public records. Laws vary by state, but most allow residents to request documents such as contracts, emails, and invoices.
|
||||
A public records request is a formal way to ask government agencies for public records. Laws vary by state, but most allow residents to request documents such as contracts, emails, and invoices.
|
||||
</p>
|
||||
|
||||
<h3>Who can file a FOIA request?</h3>
|
||||
<h3>Who can file a public records request?</h3>
|
||||
<p>
|
||||
Most states allow anyone to file a FOIA request, while some restrict it to state residents.
|
||||
Most states allow anyone to file a public records request, while some restrict it to state residents.
|
||||
</p>
|
||||
|
||||
<h3>What can be requested?</h3>
|
||||
@@ -42,18 +42,18 @@
|
||||
|
||||
<h3>Can I remain anonymous?</h3>
|
||||
<p>
|
||||
Typically, yes. Many states allow you to submit FOIA requests anonymously or through an alias. If anonymity is important to you, consider using a disposable email address and avoiding personally identifying information in your request. If you're requesting records from a state that requires residency, you may need to provide an in-state address.
|
||||
Typically, yes. Many states allow you to submit public records requests anonymously or through an alias. If anonymity is important to you, consider using a disposable email address and avoiding personally identifying information in your request. If you're requesting records from a state that requires residency, you may need to provide an in-state address.
|
||||
</p>
|
||||
|
||||
<v-divider class="my-8" />
|
||||
|
||||
<h2 id="how-to">How to File a FOIA Request</h2>
|
||||
<h2 id="how-to">How to File a Public Records Request</h2>
|
||||
<ol class="serif">
|
||||
<li class="mb-4">
|
||||
Identify the agency that manages law enforcement contracts in your area. It's most often the <b>police department</b> or <b>sheriff's office</b>. When in doubt, you can always submit a request to multiple agencies, but use your best judgment.
|
||||
</li>
|
||||
<li class="mb-4">
|
||||
Locate their FOIA or public records request portal. If they don't have one, find the relevant contact email.
|
||||
Locate their public records request portal. If they don't have one, find the relevant contact email.
|
||||
</li>
|
||||
<li class="mb-4">
|
||||
Use <b>this template</b> to submit the request. Feel free to change parts of it to better suit your situation:
|
||||
@@ -78,7 +78,7 @@
|
||||
</ol>
|
||||
|
||||
<v-alert class="mt-8" type="info" variant="tonal">
|
||||
<b>Tip:</b> If you run into any issues while submitting the FOIA request, such as not being able to locate the records portal or find a contact email, you can use <a href="https://muckrock.com" target="_blank">Muckrock</a>, a nonprofit FOIA platform that will submit it to the agency on your behalf for a small fee.
|
||||
<b>Tip:</b> If you run into any issues while submitting the public records request, such as not being able to locate the records portal or find a contact email, you can use <a href="https://muckrock.com" target="_blank">Muckrock</a>, a nonprofit public records platform that will submit it to the agency on your behalf for a small fee.
|
||||
</v-alert>
|
||||
|
||||
</v-container>
|
||||
@@ -93,10 +93,10 @@ import Hero from '@/components/layout/Hero.vue';
|
||||
import Footer from '@/components/layout/Footer.vue';
|
||||
|
||||
useSeoMeta({
|
||||
title: 'How to FOIA | DeFlock',
|
||||
title: 'How to Request Public Records | DeFlock',
|
||||
description: 'Learn how simple it is to request ALPR data from your city.',
|
||||
|
||||
ogTitle: 'How to FOIA Request ALPR Data',
|
||||
ogTitle: 'How to Request ALPR Data',
|
||||
ogDescription: 'Learn how simple it is to request ALPR data from your city.',
|
||||
ogImage: 'https://deflock.me/documents.jpg',
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user