From b197966b8feb19dc4f8c8bda4d8e52acf87d7ccd Mon Sep 17 00:00:00 2001 From: KaBankz <20971850+KaBankz@users.noreply.github.com> Date: Mon, 20 Jan 2025 19:03:43 -0500 Subject: [PATCH 01/14] Add projects api call and schema --- app/maps/actions.ts | 50 +++++++ app/maps/page.tsx | 3 + package-lock.json | 12 +- package.json | 3 +- types/projects.ts | 316 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 382 insertions(+), 2 deletions(-) create mode 100644 app/maps/actions.ts create mode 100644 app/maps/page.tsx create mode 100644 types/projects.ts diff --git a/app/maps/actions.ts b/app/maps/actions.ts new file mode 100644 index 0000000..78b22db --- /dev/null +++ b/app/maps/actions.ts @@ -0,0 +1,50 @@ +'use server'; + +import { ProjectsSchema } from '@/types/projects'; + +export async function getProjects({ + limit = 1000, + offset = 0, +}: { + /** + * The number of results to return + * + * @default 1000 + */ + limit?: number; + /** + * The index of the result array where to start the returned list of results. + * + * @default 0 + **/ + offset?: number; +}) { + const token = process.env.APP_TOKEN; + + if (!token) { + throw new Error('APP_TOKEN environment variable is not set'); + } + + try { + const response = await fetch( + `https://data.ny.gov/resource/dprp-55ye.json?$limit=${limit}&$offset=${offset}`, + { + headers: { + 'X-App-Token': token, + }, + } + ); + + if (!response.ok) { + throw new Error('API Error'); + } + + const data = await response.json(); + + const projects = ProjectsSchema.parse(data); + return projects; + } catch (error) { + console.error('Failed to fetch projects:', error); + throw error; + } +} diff --git a/app/maps/page.tsx b/app/maps/page.tsx new file mode 100644 index 0000000..c921807 --- /dev/null +++ b/app/maps/page.tsx @@ -0,0 +1,3 @@ +export default function MapsPage() { + return
Maps
; +} diff --git a/package-lock.json b/package-lock.json index 97a33eb..2984a6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,8 @@ "motion": "^11.18.1", "next": "15.1.5", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "zod": "^3.24.1" }, "devDependencies": { "@eslint/eslintrc": "^3", @@ -6540,6 +6541,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 0c26238..fd9f0cc 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "motion": "^11.18.1", "next": "15.1.5", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "zod": "^3.24.1" }, "devDependencies": { "@eslint/eslintrc": "^3", diff --git a/types/projects.ts b/types/projects.ts new file mode 100644 index 0000000..68b33b8 --- /dev/null +++ b/types/projects.ts @@ -0,0 +1,316 @@ +import { z } from 'zod'; + +export const ProjectSchema = z.object({ + /** The date the dataset was refreshed */ + data_through_date: z.coerce.date(), + + /** + * Either Maintenance, Non-Tier 1, Tier 1, Tier 2, Tier 4, OREC, or N/A. + * Maintenance represents existing baseline resources. Non-Tier 1 are + * facilities with a commercial operation date before 1/1/2015, and Tier 1 + * are facilities with a commercial operation date after 1/1/2015. Tier 2 + * facilities are from wind and non-state-owned run-of-river hydroelectric + * generating facilities located within New York State that entered + * commercial operation prior to January 1, 2015. OREC represents projects + * in the offshore wind program. Tier 4 represents renewable energy and + * transmission projects. + */ + eligibility: z.string(), + + /** + * Name of project. Project names with an asterisk have the same project + * name as projects awarded under different solicitations + */ + project_name: z.string(), + + /** + * Request for Proposal (RFP) number, or Maintenance Agreement number under + * Renewable Portfolio Standard (RPS) or Tier 2 Maintenance under the + * Renewable Energy Standard (RES) + */ + solicitation_name: z.string(), + + /** + * Certain solicitations provided for an optional or mandatory adjustment to + * the Index REC Strike Price or Fixed REC Price based on changes in + * certain commodity price indices as described in each RFP. Proposers + * awarded under these solicitations whose proposals were subject to this + * adjustment may have their Index REC Strike Price or Fixed REC Price + * adjusted consistent with the methodology described in each applicable RFP. + */ + inflation_adjustment: z.string().optional(), + + /** + * The Fixed REC bid price of Tier 1 and Non-Tier 1 awarded projects in US + * dollars. Blank cells represent data that was not required or is not + * currently available. + */ + fixed_rec_price: z.coerce.number().optional(), + + /** + * The Index OREC bid price or the Index REC Conversion Offer Price of + * Tier 1 or Off-shore wind awarded projects in US dollars. Blank cells + * represent data that was not required or is not currently available. + */ + index_orec_strike_price: z.coerce.number().optional(), + + /** + * Indicates if a Non-Tier 1 or Tier 1 Project claimed Incremental Economic + * Benefits as part of its Bid Proposal, and these claims were accepted by + * NYSERDA as part of its award. Blank cells represent data that was not + * required or is not currently available. For more information, please + * refer to each specific RFP's Section on Incremental Economic Benefits to + * New York State. + */ + incremental_economic_benefits_claimed: z.string().optional(), + + /** + * For Non-Tier 1 or Tier I Projects, indicates the verification status of + * the Incremental Economic Benefits included in the projects Agreement. + * Incremental Economic Benefits are verified following three years of + * operation and must meet a threshold of 85% of claim accepted by NYSERDA. + * Yes - indicates verification complete and project met threshold. No – + * verification complete and project did not meet threshold. Blank cells + * represent – verification not due yet/ not yet complete; no economic + * benefit claim; or project cancelled/terminated. + */ + project_met_economic_benefits_threshold: z.string().optional(), + + /** + * The type of renewable energy technology for the project currently under + * contract or installed; either Land Based Wind, Offshore Wind, + * Hydroelectric, Biomass, Biogas-LFG, Biogas-ADG, Fuel Cell, Solar, + * Geothermal, Maintenance Biomass, or Maintenance Hydroelectric. + */ + renewable_technology: z.string().optional(), + + /** + * Indicates the type of eligible generation, either Existing or New + * Generation. Blank cells represent data that was not required or is not + * currently available. + */ + generation_type: z.string().optional(), + + /** + * Vintage Generation Facility (VGS). VGFs are projects that are either + * upgraded, returned to service, relocated, or repowered, and must meet + * the specified requirements + */ + type_of_existing: z.string().optional(), + + /** + * Name of the Seller in NYSERDA's agreement. A counterparty is the other + * party to a NYSERDA agreement. Blank cells represent data that was not + * required or is not currently available. + */ + counterparty: z.string().optional(), + + /** + * Name of Developer responsible for developing the project. Blank cells + * represent data that was not required or is not currently available. + */ + developer_name: z.string().optional(), + + /** + * Rating of the electric power to be delivered from an energy storage + * project in alternating current (AC) power. Blank cells represent data + * that was not required or is not currently available. + */ + energy_storage_power_capacity_mwac: z.coerce.number().optional(), + + /** + * Usable installed energy storage capacity measured in alternating current + * (AC) power. It is equal to the total capacity measured during a complete + * discharge from a 100% usable state of charge, performed in accordance + * with the storage manufacturer's specifications, on the commercial + * operation date. Blank cells represent data that was not required or is + * not currently available. This category does not include pumped storage. + */ + energy_storage_energy_capacity_mwh: z.coerce.number().optional(), + + /** + * Indicates the New York Control Area Load Zone into which the Bid + * Facility will interconnect or deliver. Blank cells represent data that + * was not required or is not currently available. + */ + nyiso_zone: z.string().optional(), + + /** + * Indicates the NYISO Generator Point Identifier. A Point Identifier is a + * resource-specific numerical identifier used by the NYISO's software + * systems to identify Generators and other Suppliers and only applies to + * Operating Projects. Blank cells represent data that was not required or + * is not currently available. + */ + ptid: z.string().optional(), + + /** + * For the NYISO Interconnection Queue, indicates the number on the list of + * transmission and generation projects seeking to join the grid. + * Additional information is located at: + * https://www.nyiso.com/interconnections. Tier 4 projects include both + * Resources and Transmission numbers. + */ + interconnection_queue_number: z.string().optional(), + + /** + * The applicable federal, state, or local permitting requirements that the + * Bid Facility is subject to. Blank cells represent data that was not + * required or is not currently available. + */ + permit_process: z.string().optional(), + + /** + * For Offshore Wind and Tier 4 projects, the Federal Permitting Mechanism + * related to Environmental Review and Permitting. For Tier 1 projects, the + * Name of Article 10 or Article VIII Case Number. Blank cells represent + * data that were not required, applicable or are not currently available + */ + regulatory_permitting: z + .object({ + url: z.string(), + }) + .optional(), + + /** Name of Article VII pertaining to Offshore Wind Project or Tier 4 Project. */ + article_vii: z + .object({ + url: z.string(), + }) + .optional(), + + /** + * ZIP code of project. Blank cells represent data that was not required or + * is not currently available. + */ + zip_code: z.string().optional(), + + /** + * Name of US county or Canadian Province for project. Blank cells + * represent data that was not required or is not currently available. + */ + county_province: z.string().optional(), + + /** + * Name of US state or Canadian Province for project. Blank cells represent + * data that was not required or is not currently available. + */ + state_province: z.string().optional(), + + /** + * Regional Economic Development Council of project. In 2011, 10 Regional + * Councils were established and charged with developing long-term + * strategic plans for economic growth in their regions. Blank cells + * indicate the project is not located in New York State. + */ + redc: z.string().optional(), + + /** + * The phase/status of which the project is in as of the Data Through + * Date; either Cancelled, Completed, Under Development, or Operational. A + * status of Cancelled means that NYSERDA's award or contract with the + * counterparty was cancelled or terminated. A status of Completed means + * the project has fulfilled their contractual obligation to NYSERDA. A + * status of Under Development means that the project has not yet entered + * operation. A status of Operational means that the project has entered + * operation. + */ + project_status: z.string(), + + /** + * Date declared as the Commercial Operation date of facility by NYISO or + * local utility. Blank cells represent data that was not required or is + * not currently available. + */ + year_of_commercial_operation: z.string().optional(), + + /** + * Date NYSERDA's payments started or are expected to start. For projects + * Under Development, the dates are reflective of the current status of + * NYSERDA agreements and are subject to change based on project + * development progress. Blank cells represent data that was not required + * or is not currently available. + */ + year_of_delivery_start_date: z.string().optional(), + + /** Number of years of performance under the NYSERDA Agreement. */ + contract_duration: z.coerce.number().optional(), + + /** + * For a new facility, the New Renewable Capacity equals the nameplate + * capacity of the installed equipment. For a repowered facility, End of + * Useful Life Bid Capacity is reported. For an upgraded facility, only the + * renewable capacity resulting from the upgrade of a facility is provided. + * For a single facility that was awarded multiple contracts, each for a + * percentage of the facility's output under multiple RFPs, the total New + * Renewable Capacity for the facility is listed once to avoid + * over-counting. Therefore, the same facility appearing in multiple + * instances may show a New Renewable Capacity of zero. Blank cells + * represent projects with expired contracts with NYSERDA. + */ + new_renewable_capacity_mw: z.coerce.number().optional(), + + /** + * The NYSERDA-contracted portion of the New Renewable Capacity in + * Megawatts. Blank cells represent data that were not applicable, + * required or are not currently available. + */ + bid_capacity_mw: z.coerce.number().optional(), + + /** + * The annual production of the project in Megawatt hours that NYSERDA and + * the Counterparty agree on constitutes performance in an Agreement. The + * Bid Quantity may not constitute the entire output of a project. Blank + * cells represent data that was not required or is not currently + * available. + */ + bid_quantity_mwh: z.coerce.number().optional(), + + /** + * The maximum annual contractual obligation of the project in Megawatt + * hours. Blank cells represent data that was not required or is not + * currently available. + */ + max_annual_contract_quantity: z.coerce.number().optional(), + + /** + * An amount of electrical energy (in MWh), such that the estimated + * probability in any given year that generation from the Selected Project + * delivered to the Delivery Point would exceed that amount is 10 percent. + * Applicable to Offshore Wind projects only. Blank cells represent data + * that was not required or is not currently available. + */ + p10_annual_orec_exceedance: z.coerce.number().optional(), + + /** + * A NYSERDA calculation based on a 10.5 meter/second average Hub Height + * (m/s avgHH) wind speed. Weibull parameters of A = 11.3 and K = 2.2 were + * used to create an annual wind frequency distribution table. The + * resulting frequency distribution was then applied in the NREL 15 MW + * turbine power curve assuming 22% losses. Blank cells represent data + * there were not required or is not currently available. + */ + p50_generation_calculated_by_nyserda_mwh_yr: z.coerce.number().optional(), + + /** + * Capacity of a project's new HVDC transmission line. Applicable to Tier 4 + * projects only. Blank cells represent data that was not required or is + * not currently available. + */ + transmission_capacity_hvdc: z.coerce.number().optional(), + + /** + * Open Data/Socrata-generated geocoding information based on supplied + * address components. + */ + georeference: z + .object({ + coordinates: z.tuple([z.number(), z.number()]), + type: z.literal('Point'), + }) + .optional(), +}); + +export type Project = z.infer; + +export const ProjectsSchema = z.array(ProjectSchema); From c8114061160842bb4d58dcff57e25c41a3f7c41d Mon Sep 17 00:00:00 2001 From: KaBankz <20971850+KaBankz@users.noreply.github.com> Date: Mon, 20 Jan 2025 19:43:23 -0500 Subject: [PATCH 02/14] Add outages api call and schema --- app/maps/actions.ts | 55 +++++++++-- types/outages.ts | 236 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 284 insertions(+), 7 deletions(-) create mode 100644 types/outages.ts diff --git a/app/maps/actions.ts b/app/maps/actions.ts index 78b22db..47c78e3 100644 --- a/app/maps/actions.ts +++ b/app/maps/actions.ts @@ -1,7 +1,14 @@ 'use server'; +import { OutagesSchema } from '@/types/outages'; import { ProjectsSchema } from '@/types/projects'; +const SOCRATA_APP_TOKEN = ''; + +// if (!SOCRATA_APP_TOKEN) { +// throw new Error('SOCRATA_APP_TOKEN environment variable is not set'); +// } + export async function getProjects({ limit = 1000, offset = 0, @@ -19,18 +26,12 @@ export async function getProjects({ **/ offset?: number; }) { - const token = process.env.APP_TOKEN; - - if (!token) { - throw new Error('APP_TOKEN environment variable is not set'); - } - try { const response = await fetch( `https://data.ny.gov/resource/dprp-55ye.json?$limit=${limit}&$offset=${offset}`, { headers: { - 'X-App-Token': token, + 'X-App-Token': SOCRATA_APP_TOKEN, }, } ); @@ -48,3 +49,43 @@ export async function getProjects({ throw error; } } + +export async function getOutages({ + limit = 1000, + offset = 0, +}: { + /** + * The number of results to return + * + * @default 1000 + */ + limit?: number; + /** + * The index of the result array where to start the returned list of results. + * + * @default 0 + **/ + offset?: number; +}) { + try { + const response = await fetch( + `https://data.cityofnewyork.us/resource/br6j-yp22.json?$limit=${limit}&$offset=${offset}`, + { + headers: { + 'X-App-Token': SOCRATA_APP_TOKEN, + }, + } + ); + + if (!response.ok) { + throw new Error('API Error'); + } + + const data = await response.json(); + const outages = OutagesSchema.parse(data); + return outages; + } catch (error) { + console.error('Failed to fetch outages:', error); + throw error; + } +} diff --git a/types/outages.ts b/types/outages.ts new file mode 100644 index 0000000..70633f8 --- /dev/null +++ b/types/outages.ts @@ -0,0 +1,236 @@ +import { z } from 'zod'; + +/* Helper function to coerce an object to a zod schema **/ +function coerceObject( + shape: T, + params?: z.RawCreateParams +) { + return new z.ZodEffects({ + schema: z.object(shape, params), + effect: { type: 'preprocess', transform: Object }, + typeName: z.ZodFirstPartyTypeKind.ZodEffects, + }); +} + +export const OutageSchema = z.object({ + /** + * Unique identifier of a Service Request (SR) in the open data set + */ + unique_key: z.string(), + + /** + * Date SR was created + */ + created_date: z.coerce.date(), + + /** + * Date SR was closed by responding agency + */ + closed_date: z.coerce.date().optional(), + + /** + * Acronym of responding City Government Agency + */ + agency: z.string(), + + /** + * Full Agency name of responding City Government Agency + */ + agency_name: z.string(), + + /** + * This is the first level of a hierarchy identifying the topic of the + * incident or condition. Complaint Type may have a corresponding + * Descriptor (below) or may stand alone. + */ + complaint_type: z.string(), + + /** + * This is associated to the Complaint Type, and provides further detail on + * the incident or condition. Descriptor values are dependent on the + * Complaint Type, and are not always required in SR. + */ + descriptor: z.string(), + + /** + * Describes the type of location used in the address information + */ + location_type: z.string(), + + /** + * Incident location zip code, provided by geo validation. + */ + incident_zip: z.coerce.number().optional(), + + /** + * House number of incident address provided by submitter. + */ + incident_address: z.string(), + + /** + * Street name of incident address provided by the submitter + */ + street_name: z.string(), + + /** + * First Cross street based on the geo validated incident location + */ + cross_street_1: z.string().optional(), + + /** + * Second Cross Street based on the geo validated incident location + */ + cross_street_2: z.string().optional(), + + /** + * First intersecting street based on geo validated incident location + */ + intersection_street_1: z.string().optional(), + + /** + * Second intersecting street based on geo validated incident location + */ + intersection_street_2: z.string().optional(), + + /** + * Type of incident location information available. + */ + address_type: z.string().optional(), + + /** + * City of the incident location provided by geovalidation. + */ + city: z.string().optional(), + + /** + * If the incident location is identified as a Landmark the name of the + * landmark will display here + */ + landmark: z.string().optional(), + + /** + * If available, this field describes the type of city facility associated + * to the SR + */ + facility_type: z.string().optional(), + + /** + * Status of SR submitted + */ + status: z.string(), + + /** + * Date when responding agency is expected to update the SR. This is based + * on the Complaint Type and internal Service Level Agreements (SLAs). + */ + due_date: z.coerce.date().optional(), + + /** + * Date when responding agency last updated the SR. + */ + resolution_action_updated_date: z.coerce.date().optional(), + + /** + * Provided by geovalidation. + */ + community_board: z.string(), + + /** + * Provided by the submitter and confirmed by geovalidation. + */ + borough: z.string(), + + /** + * Geo validated, X coordinate of the incident location. + */ + x_coordinate_state_plane: z.coerce.number().optional(), + + /** + * Geo validated, Y coordinate of the incident location. + */ + y_coordinate_state_plane: z.coerce.number().optional(), + + /** + * If the incident location is a Parks Dept facility, the Name of the + * facility will appear here + */ + park_facility_name: z.string(), + + /** + * The borough of incident if it is a Parks Dept facility + */ + park_borough: z.string(), + + /** + * If the incident is a taxi, this field describes the type of TLC + * vehicle. + */ + vehicle_type: z.string().optional(), + + /** + * If the incident is identified as a taxi, this field will display the + * borough of the taxi company. + */ + taxi_company_borough: z.string().optional(), + + /** + * If the incident is identified as a taxi, this field displays the taxi + * pick up location + */ + taxi_pick_up_location: z.string().optional(), + + /** + * If the incident is identified as a Bridge/Highway, the name will be + * displayed here. + */ + bridge_highway_name: z.string().optional(), + + /** + * If the incident is identified as a Bridge/Highway, the direction where + * the issue took place would be displayed here. + */ + bridge_highway_direction: z.string().optional(), + + /** + * If the incident location was Bridge/Highway this column differentiates + * if the issue was on the Road or the Ramp. + */ + road_ramp: z.string().optional(), + + /** + * Additional information on the section of the Bridge/Highway were the + * incident took place. + */ + bridge_highway_segment: z.string().optional(), + + /** + * Geo based Lat of the incident location + */ + latitude: z.coerce.number().optional(), + + /** + * Geo based Long of the incident location + */ + longitude: z.coerce.number().optional(), + + /** + * Combination of the geo based lat & long of the incident location + */ + location: z + .object({ + latitude: z.coerce.number(), + longitude: z.coerce.number(), + needs_recoding: z.boolean(), + human_address: coerceObject({ + address: z.string().optional(), + city: z.string().optional(), + state: z.string().optional(), + zip: z.coerce.number().optional(), + }), + }) + .optional(), +}); + +export type Outage = z.infer; + +export const OutagesSchema = z.array(OutageSchema); From 38da29fce1903621e69f051a81f1a19b75c7932b Mon Sep 17 00:00:00 2001 From: KaBankz <20971850+KaBankz@users.noreply.github.com> Date: Mon, 20 Jan 2025 19:55:20 -0500 Subject: [PATCH 03/14] Add evChargingStations api call and schema --- app/maps/actions.ts | 41 ++++++++++++ types/evChargingStations.ts | 127 ++++++++++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 types/evChargingStations.ts diff --git a/app/maps/actions.ts b/app/maps/actions.ts index 47c78e3..63aa1ad 100644 --- a/app/maps/actions.ts +++ b/app/maps/actions.ts @@ -1,5 +1,6 @@ 'use server'; +import { EVChargingStationsSchema } from '@/types/evChargingStations'; import { OutagesSchema } from '@/types/outages'; import { ProjectsSchema } from '@/types/projects'; @@ -89,3 +90,43 @@ export async function getOutages({ throw error; } } + +export async function getEVChargingStations({ + limit = 1000, + offset = 0, +}: { + /** + * The number of results to return + * + * @default 1000 + */ + limit?: number; + /** + * The index of the result array where to start the returned list of results. + * + * @default 0 + **/ + offset?: number; +}) { + try { + const response = await fetch( + `https://data.cityofnewyork.us/resource/fc53-9hrv.json?$limit=${limit}&$offset=${offset}`, + { + headers: { + 'X-App-Token': SOCRATA_APP_TOKEN, + }, + } + ); + + if (!response.ok) { + throw new Error('API Error'); + } + + const data = await response.json(); + const evChargingStations = EVChargingStationsSchema.parse(data); + return evChargingStations; + } catch (error) { + console.error('Failed to fetch ev charging stations:', error); + throw error; + } +} diff --git a/types/evChargingStations.ts b/types/evChargingStations.ts new file mode 100644 index 0000000..ed3cb68 --- /dev/null +++ b/types/evChargingStations.ts @@ -0,0 +1,127 @@ +import { z } from 'zod'; + +export const EVChargingStationSchema = z.object({ + /** + * Acronym/Initialism of City agency or entity the station is located at + */ + agency: z.string(), + + /** + * Street number of station's address. + */ + station_name: z.string(), + + /** + * The type of charger at the station + */ + type_of_charger: z.string(), + + /** + * The # of ports at the station + */ + no_of_ports: z.coerce.number(), + + /** + * The street address of the station + */ + street: z.string().optional(), + + /** + * The city of the station's location. + */ + city: z.string().optional(), + + /** + * ZIP code of the station's location. + */ + postcode: z.coerce.number().optional(), + + /** + * New York City's boroughs are five county-level administrative divisions, + * with each one also being a state county. (Manhattan - New York County; + * Bronx - Bronx County; Brooklyn - Kings County; Queens - Queens County; + * Staten Island - Richmond County). NYC's boroughs have existed since the + * consolidation of the city in 1898. + */ + borough: z.string().optional(), + + /** + * Is the charging station open to the general public? + */ + public_charger: z.string().optional(), + + /** + * Is there a fee to use the charger at the station for City drivers? + */ + fee_for_city_drivers: z.string().optional(), + + /** + * Latitude of station's address. + */ + latitude: z.coerce.number().optional(), + + /** + * Longitude of station's address. + */ + longitude: z.coerce.number().optional(), + + /** + * Community Districts are geographic areas created under the City Charter + * that are used for the delivery of municipal services, to facilitate + * community engagement, and as a common unit of analysis. The City is + * divided into 59 community districts. Each Community District has a + * Community Board. + */ + community_district: z.coerce.number().optional(), + + /** + * The City Council is the lawmaking body of NYC, on equal footing with + * the mayor in terms of governing power. Besides legislating, the Council + * has sole approval power over the city budget, and is the final + * decision-maker in land use matters. There are 51 City Council members + * in total, each representing a unique geographical area, called a + * Council District. + */ + council_district: z.coerce.number().optional(), + + /** + * Geographic area defined by the U.S. Census Bureau for the various + * decennial censuses. Census tracts for a particular census year are + * numbered uniquely within borough. Please note that as part of the + * geocoding process, leading and trailing zeros are dropped. + */ + census_tract: z.coerce.number().optional(), + + /** + * A unique 7-digit number assigned to every known building by the + * Department of City Planning (DCP), the first digit of which is the + * borough code. BINs allow city agencies to process and match + * building-related data easily and in a consistent manner. + */ + bin: z.coerce.number().optional(), + + /** + * A combination of three numeric codes -- a 1-digit borough number, a + * block number (up to 5 digits) and a lot number (up to 4 digits) -- + * designated and modified by the Department of Finance (DOF). BBLs are + * used by various city agencies to identify real estate for taxes, + * zoning, construction, and other purposes. + */ + bbl: z.coerce.number().optional(), + + /** + * NTAs are small area boundaries, created by the Department of City + * Planning (DCP) to aggregate population projections in a small area. + * Each NTA approximates a minimum population of 15,000. While NTAs were + * initially created to support PlaNYC, the thirty-year (2000-2030) + * sustainability plan for NYC, NTAs are now also being used to present + * data from the Decennial Census and American Community Survey. NTA + * boundaries and their associated names do not definitively represent + * neighborhood boundaries. + */ + nta: z.string().optional(), +}); + +export type EVChargingStation = z.infer; + +export const EVChargingStationsSchema = z.array(EVChargingStationSchema); From d9cca985f431df5b519b335fa7a68f219eb6fcad Mon Sep 17 00:00:00 2001 From: KaBankz <20971850+KaBankz@users.noreply.github.com> Date: Mon, 20 Jan 2025 20:32:55 -0500 Subject: [PATCH 04/14] Add ratSightings api call and schema --- app/maps/actions.ts | 41 ++++++++ types/ratSightings.ts | 222 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 263 insertions(+) create mode 100644 types/ratSightings.ts diff --git a/app/maps/actions.ts b/app/maps/actions.ts index 63aa1ad..797ef44 100644 --- a/app/maps/actions.ts +++ b/app/maps/actions.ts @@ -3,6 +3,7 @@ import { EVChargingStationsSchema } from '@/types/evChargingStations'; import { OutagesSchema } from '@/types/outages'; import { ProjectsSchema } from '@/types/projects'; +import { RatSightingsSchema } from '@/types/ratSightings'; const SOCRATA_APP_TOKEN = ''; @@ -130,3 +131,43 @@ export async function getEVChargingStations({ throw error; } } + +export async function getRatSightings({ + limit = 1000, + offset = 0, +}: { + /** + * The number of results to return + * + * @default 1000 + */ + limit?: number; + /** + * The index of the result array where to start the returned list of results. + * + * @default 0 + **/ + offset?: number; +}) { + try { + const response = await fetch( + `https://data.cityofnewyork.us/resource/3q43-55fe.json?$limit=${limit}&$offset=${offset}`, + { + headers: { + 'X-App-Token': SOCRATA_APP_TOKEN, + }, + } + ); + + if (!response.ok) { + throw new Error('API Error'); + } + + const data = await response.json(); + const ratSightings = RatSightingsSchema.parse(data); + return ratSightings; + } catch (error) { + console.error('Failed to fetch rat sightings:', error); + throw error; + } +} diff --git a/types/ratSightings.ts b/types/ratSightings.ts new file mode 100644 index 0000000..5813769 --- /dev/null +++ b/types/ratSightings.ts @@ -0,0 +1,222 @@ +import { z } from 'zod'; + +export const RatSightingSchema = z.object({ + /** + * Unique identifier of a Service Request (SR) in the open data set + */ + unique_key: z.coerce.number(), + + /** + * Date SR was created + */ + created_date: z.coerce.date(), + + /** + * Date SR was closed by responding agency + */ + closed_date: z.coerce.date().optional(), + + /** + * Acronym of responding City Government Agency + */ + agency: z.string(), + + /** + * Full Agency name of responding City Government Agency + */ + agency_name: z.string(), + + /** + * This is the first level of a hierarchy identifying the topic of the + * incident or condition. Complaint Type may have a corresponding + * Descriptor (below) or may stand alone. + */ + complaint_type: z.string(), + + /** + * This is associated to the Complaint Type, and provides further detail + * on the incident or condition. Descriptor values are dependent on the + * Complaint Type, and are not always required in SR. + */ + descriptor: z.string(), + + /** + * Describes the type of location used in the address information + */ + location_type: z.string().optional(), + + /** + * Incident location zip code, provided by geo validation. + */ + // Handle extremely rare edge case, where the string 'N/A' is returned + incident_zip: z.preprocess( + (val) => (val === 'N/A' ? undefined : val), + z.coerce.number().optional() + ), + + /** + * House number of incident address provided by submitter. + */ + incident_address: z.string().optional(), + + /** + * Street name of incident address provided by the submitter + */ + street_name: z.string().optional(), + + /** + * First Cross street based on the geo validated incident location + */ + cross_street_1: z.string().optional(), + + /** + * Second Cross Street based on the geo validated incident location + */ + cross_street_2: z.string().optional(), + + /** + * First intersecting street based on geo validated incident location + */ + intersection_street_1: z.string().optional(), + + /** + * Second intersecting street based on geo validated incident location + */ + intersection_street_2: z.string().optional(), + + /** + * Type of incident location information available. + */ + address_type: z.string().optional(), + + /** + * City of the incident location provided by geovalidation. + */ + city: z.string().optional(), + + /** + * If the incident location is identified as a Landmark the name of the + * landmark will display here + */ + landmark: z.string().optional(), + + /** + * If available, this field describes the type of city facility + * associated to the SR + */ + facility_type: z.string().optional(), + + /** + * Status of SR submitted + */ + status: z.string(), + + /** + * Date when responding agency is expected to update the SR. This is + * based on the Complaint Type and internal Service Level Agreements + * (SLAs). + */ + due_date: z.coerce.date().optional(), + + /** + * Date when responding agency last updated the SR. + */ + resolution_action_updated_date: z.coerce.date().optional(), + + /** + * Provided by geovalidation. + */ + community_board: z.string().optional(), + + /** + * Provided by the submitter and confirmed by geovalidation. + */ + borough: z.string().optional(), + + /** + * Geo validated, X coordinate of the incident location. + */ + x_coordinate_state_plane: z.coerce.number().optional(), + + /** + * Geo validated, Y coordinate of the incident location. + */ + y_coordinate_state_plane: z.coerce.number().optional(), + + /** + * If the incident location is a Parks Dept facility, the Name of the + * facility will appear here + */ + park_facility_name: z.string(), + + /** + * The borough of incident if it is a Parks Dept facility + */ + park_borough: z.string().optional(), + + /** + * If the incident is a taxi, this field describes the type of TLC + * vehicle. + */ + vehicle_type: z.string().optional(), + + /** + * If the incident is identified as a taxi, this field will display the + * borough of the taxi company. + */ + taxi_company_borough: z.string().optional(), + + /** + * If the incident is identified as a taxi, this field displays the taxi + * pick up location + */ + taxi_pick_up_location: z.string().optional(), + + /** + * If the incident is identified as a Bridge/Highway, the name will be + * displayed here. + */ + bridge_highway_name: z.string().optional(), + + /** + * If the incident is identified as a Bridge/Highway, the direction where + * the issue took place would be displayed here. + */ + bridge_highway_direction: z.string().optional(), + + /** + * If the incident location was Bridge/Highway this column differentiates + * if the issue was on the Road or the Ramp. + */ + road_ramp: z.string().optional(), + + /** + * Additional information on the section of the Bridge/Highway were the + * incident took place. + */ + bridge_highway_segment: z.string().optional(), + + /** + * Geo based Lat of the incident location + */ + latitude: z.coerce.number().optional(), + + /** + * Geo based Long of the incident location + */ + longitude: z.coerce.number().optional(), + + /** + * Combination of the geo based lat & long of the incident location + */ + location: z + .object({ + coordinates: z.tuple([z.number(), z.number()]), + type: z.literal('Point'), + }) + .optional(), +}); + +export type RatSighting = z.infer; + +export const RatSightingsSchema = z.array(RatSightingSchema); From 025528e3e9c13f2073fbdaf474c7443cf1e3852f Mon Sep 17 00:00:00 2001 From: KaBankz <20971850+KaBankz@users.noreply.github.com> Date: Mon, 20 Jan 2025 21:20:48 -0500 Subject: [PATCH 05/14] Make schema parsing strict and add missing keys Some of the missing keys look kinda temporary, so this may cause some issues down the line. We can make it less strict if need be. --- types/evChargingStations.ts | 247 +++++++------- types/outages.ts | 446 ++++++++++++------------- types/projects.ts | 629 ++++++++++++++++++------------------ types/ratSightings.ts | 444 ++++++++++++------------- 4 files changed, 901 insertions(+), 865 deletions(-) diff --git a/types/evChargingStations.ts b/types/evChargingStations.ts index ed3cb68..b89009a 100644 --- a/types/evChargingStations.ts +++ b/types/evChargingStations.ts @@ -1,126 +1,131 @@ import { z } from 'zod'; -export const EVChargingStationSchema = z.object({ - /** - * Acronym/Initialism of City agency or entity the station is located at - */ - agency: z.string(), - - /** - * Street number of station's address. - */ - station_name: z.string(), - - /** - * The type of charger at the station - */ - type_of_charger: z.string(), - - /** - * The # of ports at the station - */ - no_of_ports: z.coerce.number(), - - /** - * The street address of the station - */ - street: z.string().optional(), - - /** - * The city of the station's location. - */ - city: z.string().optional(), - - /** - * ZIP code of the station's location. - */ - postcode: z.coerce.number().optional(), - - /** - * New York City's boroughs are five county-level administrative divisions, - * with each one also being a state county. (Manhattan - New York County; - * Bronx - Bronx County; Brooklyn - Kings County; Queens - Queens County; - * Staten Island - Richmond County). NYC's boroughs have existed since the - * consolidation of the city in 1898. - */ - borough: z.string().optional(), - - /** - * Is the charging station open to the general public? - */ - public_charger: z.string().optional(), - - /** - * Is there a fee to use the charger at the station for City drivers? - */ - fee_for_city_drivers: z.string().optional(), - - /** - * Latitude of station's address. - */ - latitude: z.coerce.number().optional(), - - /** - * Longitude of station's address. - */ - longitude: z.coerce.number().optional(), - - /** - * Community Districts are geographic areas created under the City Charter - * that are used for the delivery of municipal services, to facilitate - * community engagement, and as a common unit of analysis. The City is - * divided into 59 community districts. Each Community District has a - * Community Board. - */ - community_district: z.coerce.number().optional(), - - /** - * The City Council is the lawmaking body of NYC, on equal footing with - * the mayor in terms of governing power. Besides legislating, the Council - * has sole approval power over the city budget, and is the final - * decision-maker in land use matters. There are 51 City Council members - * in total, each representing a unique geographical area, called a - * Council District. - */ - council_district: z.coerce.number().optional(), - - /** - * Geographic area defined by the U.S. Census Bureau for the various - * decennial censuses. Census tracts for a particular census year are - * numbered uniquely within borough. Please note that as part of the - * geocoding process, leading and trailing zeros are dropped. - */ - census_tract: z.coerce.number().optional(), - - /** - * A unique 7-digit number assigned to every known building by the - * Department of City Planning (DCP), the first digit of which is the - * borough code. BINs allow city agencies to process and match - * building-related data easily and in a consistent manner. - */ - bin: z.coerce.number().optional(), - - /** - * A combination of three numeric codes -- a 1-digit borough number, a - * block number (up to 5 digits) and a lot number (up to 4 digits) -- - * designated and modified by the Department of Finance (DOF). BBLs are - * used by various city agencies to identify real estate for taxes, - * zoning, construction, and other purposes. - */ - bbl: z.coerce.number().optional(), - - /** - * NTAs are small area boundaries, created by the Department of City - * Planning (DCP) to aggregate population projections in a small area. - * Each NTA approximates a minimum population of 15,000. While NTAs were - * initially created to support PlaNYC, the thirty-year (2000-2030) - * sustainability plan for NYC, NTAs are now also being used to present - * data from the Decennial Census and American Community Survey. NTA - * boundaries and their associated names do not definitively represent - * neighborhood boundaries. - */ - nta: z.string().optional(), -}); +export const EVChargingStationSchema = z + .object({ + /** + * Acronym/Initialism of City agency or entity the station is located at + */ + agency: z.string(), + + /** + * Street number of station's address. + */ + station_name: z.string(), + + /** + * The type of charger at the station + */ + type_of_charger: z.string(), + + /** + * The # of ports at the station + */ + no_of_ports: z.coerce.number(), + + /** + * The street address of the station + */ + street: z.string().optional(), + + /** + * The city of the station's location. + */ + city: z.string().optional(), + + /** + * ZIP code of the station's location. + */ + postcode: z.coerce.number().optional(), + + /** + * New York City's boroughs are five county-level administrative divisions, + * with each one also being a state county. (Manhattan - New York County; + * Bronx - Bronx County; Brooklyn - Kings County; Queens - Queens County; + * Staten Island - Richmond County). NYC's boroughs have existed since the + * consolidation of the city in 1898. + */ + borough: z.string().optional(), + + /** + * Is the charging station open to the general public? + */ + public_charger: z.string().optional(), + + /** + * Is there a fee to use the charger at the station for City drivers? + */ + fee_for_city_drivers: z.string().optional(), + + /** + * Latitude of station's address. + */ + latitude: z.coerce.number().optional(), + + /** + * Longitude of station's address. + */ + longitude: z.coerce.number().optional(), + + /** + * Community Districts are geographic areas created under the City Charter + * that are used for the delivery of municipal services, to facilitate + * community engagement, and as a common unit of analysis. The City is + * divided into 59 community districts. Each Community District has a + * Community Board. + */ + community_district: z.coerce.number().optional(), + + /** + * The City Council is the lawmaking body of NYC, on equal footing with + * the mayor in terms of governing power. Besides legislating, the Council + * has sole approval power over the city budget, and is the final + * decision-maker in land use matters. There are 51 City Council members + * in total, each representing a unique geographical area, called a + * Council District. + */ + council_district: z.coerce.number().optional(), + + /** + * Geographic area defined by the U.S. Census Bureau for the various + * decennial censuses. Census tracts for a particular census year are + * numbered uniquely within borough. Please note that as part of the + * geocoding process, leading and trailing zeros are dropped. + */ + census_tract: z.coerce.number().optional(), + + /** + * A unique 7-digit number assigned to every known building by the + * Department of City Planning (DCP), the first digit of which is the + * borough code. BINs allow city agencies to process and match + * building-related data easily and in a consistent manner. + */ + bin: z.coerce.number().optional(), + + /** + * A combination of three numeric codes -- a 1-digit borough number, a + * block number (up to 5 digits) and a lot number (up to 4 digits) -- + * designated and modified by the Department of Finance (DOF). BBLs are + * used by various city agencies to identify real estate for taxes, + * zoning, construction, and other purposes. + */ + bbl: z.coerce.number().optional(), + + /** + * NTAs are small area boundaries, created by the Department of City + * Planning (DCP) to aggregate population projections in a small area. + * Each NTA approximates a minimum population of 15,000. While NTAs were + * initially created to support PlaNYC, the thirty-year (2000-2030) + * sustainability plan for NYC, NTAs are now also being used to present + * data from the Decennial Census and American Community Survey. NTA + * boundaries and their associated names do not definitively represent + * neighborhood boundaries. + */ + nta: z.string().optional(), + + // Unknown Keys - keys that are not in the docs but are in the data + public_charger_: z.string().optional(), + }) + .strict(); export type EVChargingStation = z.infer; diff --git a/types/outages.ts b/types/outages.ts index 70633f8..16d5e9c 100644 --- a/types/outages.ts +++ b/types/outages.ts @@ -12,224 +12,234 @@ function coerceObject( }); } -export const OutageSchema = z.object({ - /** - * Unique identifier of a Service Request (SR) in the open data set - */ - unique_key: z.string(), - - /** - * Date SR was created - */ - created_date: z.coerce.date(), - - /** - * Date SR was closed by responding agency - */ - closed_date: z.coerce.date().optional(), - - /** - * Acronym of responding City Government Agency - */ - agency: z.string(), - - /** - * Full Agency name of responding City Government Agency - */ - agency_name: z.string(), - - /** - * This is the first level of a hierarchy identifying the topic of the - * incident or condition. Complaint Type may have a corresponding - * Descriptor (below) or may stand alone. - */ - complaint_type: z.string(), - - /** - * This is associated to the Complaint Type, and provides further detail on - * the incident or condition. Descriptor values are dependent on the - * Complaint Type, and are not always required in SR. - */ - descriptor: z.string(), - - /** - * Describes the type of location used in the address information - */ - location_type: z.string(), - - /** - * Incident location zip code, provided by geo validation. - */ - incident_zip: z.coerce.number().optional(), - - /** - * House number of incident address provided by submitter. - */ - incident_address: z.string(), - - /** - * Street name of incident address provided by the submitter - */ - street_name: z.string(), - - /** - * First Cross street based on the geo validated incident location - */ - cross_street_1: z.string().optional(), - - /** - * Second Cross Street based on the geo validated incident location - */ - cross_street_2: z.string().optional(), - - /** - * First intersecting street based on geo validated incident location - */ - intersection_street_1: z.string().optional(), - - /** - * Second intersecting street based on geo validated incident location - */ - intersection_street_2: z.string().optional(), - - /** - * Type of incident location information available. - */ - address_type: z.string().optional(), - - /** - * City of the incident location provided by geovalidation. - */ - city: z.string().optional(), - - /** - * If the incident location is identified as a Landmark the name of the - * landmark will display here - */ - landmark: z.string().optional(), - - /** - * If available, this field describes the type of city facility associated - * to the SR - */ - facility_type: z.string().optional(), - - /** - * Status of SR submitted - */ - status: z.string(), - - /** - * Date when responding agency is expected to update the SR. This is based - * on the Complaint Type and internal Service Level Agreements (SLAs). - */ - due_date: z.coerce.date().optional(), - - /** - * Date when responding agency last updated the SR. - */ - resolution_action_updated_date: z.coerce.date().optional(), - - /** - * Provided by geovalidation. - */ - community_board: z.string(), - - /** - * Provided by the submitter and confirmed by geovalidation. - */ - borough: z.string(), - - /** - * Geo validated, X coordinate of the incident location. - */ - x_coordinate_state_plane: z.coerce.number().optional(), - - /** - * Geo validated, Y coordinate of the incident location. - */ - y_coordinate_state_plane: z.coerce.number().optional(), - - /** - * If the incident location is a Parks Dept facility, the Name of the - * facility will appear here - */ - park_facility_name: z.string(), - - /** - * The borough of incident if it is a Parks Dept facility - */ - park_borough: z.string(), - - /** - * If the incident is a taxi, this field describes the type of TLC - * vehicle. - */ - vehicle_type: z.string().optional(), - - /** - * If the incident is identified as a taxi, this field will display the - * borough of the taxi company. - */ - taxi_company_borough: z.string().optional(), - - /** - * If the incident is identified as a taxi, this field displays the taxi - * pick up location - */ - taxi_pick_up_location: z.string().optional(), - - /** - * If the incident is identified as a Bridge/Highway, the name will be - * displayed here. - */ - bridge_highway_name: z.string().optional(), - - /** - * If the incident is identified as a Bridge/Highway, the direction where - * the issue took place would be displayed here. - */ - bridge_highway_direction: z.string().optional(), - - /** - * If the incident location was Bridge/Highway this column differentiates - * if the issue was on the Road or the Ramp. - */ - road_ramp: z.string().optional(), - - /** - * Additional information on the section of the Bridge/Highway were the - * incident took place. - */ - bridge_highway_segment: z.string().optional(), - - /** - * Geo based Lat of the incident location - */ - latitude: z.coerce.number().optional(), - - /** - * Geo based Long of the incident location - */ - longitude: z.coerce.number().optional(), - - /** - * Combination of the geo based lat & long of the incident location - */ - location: z - .object({ - latitude: z.coerce.number(), - longitude: z.coerce.number(), - needs_recoding: z.boolean(), - human_address: coerceObject({ - address: z.string().optional(), - city: z.string().optional(), - state: z.string().optional(), - zip: z.coerce.number().optional(), - }), - }) - .optional(), -}); +export const OutageSchema = z + .object({ + /** + * Unique identifier of a Service Request (SR) in the open data set + */ + unique_key: z.string(), + + /** + * Date SR was created + */ + created_date: z.coerce.date(), + + /** + * Date SR was closed by responding agency + */ + closed_date: z.coerce.date().optional(), + + /** + * Acronym of responding City Government Agency + */ + agency: z.string(), + + /** + * Full Agency name of responding City Government Agency + */ + agency_name: z.string(), + + /** + * This is the first level of a hierarchy identifying the topic of the + * incident or condition. Complaint Type may have a corresponding + * Descriptor (below) or may stand alone. + */ + complaint_type: z.string(), + + /** + * This is associated to the Complaint Type, and provides further detail on + * the incident or condition. Descriptor values are dependent on the + * Complaint Type, and are not always required in SR. + */ + descriptor: z.string(), + + /** + * Describes the type of location used in the address information + */ + location_type: z.string(), + + /** + * Incident location zip code, provided by geo validation. + */ + incident_zip: z.coerce.number().optional(), + + /** + * House number of incident address provided by submitter. + */ + incident_address: z.string(), + + /** + * Street name of incident address provided by the submitter + */ + street_name: z.string(), + + /** + * First Cross street based on the geo validated incident location + */ + cross_street_1: z.string().optional(), + + /** + * Second Cross Street based on the geo validated incident location + */ + cross_street_2: z.string().optional(), + + /** + * First intersecting street based on geo validated incident location + */ + intersection_street_1: z.string().optional(), + + /** + * Second intersecting street based on geo validated incident location + */ + intersection_street_2: z.string().optional(), + + /** + * Type of incident location information available. + */ + address_type: z.string().optional(), + + /** + * City of the incident location provided by geovalidation. + */ + city: z.string().optional(), + + /** + * If the incident location is identified as a Landmark the name of the + * landmark will display here + */ + landmark: z.string().optional(), + + /** + * If available, this field describes the type of city facility associated + * to the SR + */ + facility_type: z.string().optional(), + + /** + * Status of SR submitted + */ + status: z.string(), + + /** + * Date when responding agency is expected to update the SR. This is based + * on the Complaint Type and internal Service Level Agreements (SLAs). + */ + due_date: z.coerce.date().optional(), + + /** + * Date when responding agency last updated the SR. + */ + resolution_action_updated_date: z.coerce.date().optional(), + + /** + * Provided by geovalidation. + */ + community_board: z.string(), + + /** + * Provided by the submitter and confirmed by geovalidation. + */ + borough: z.string(), + + /** + * Geo validated, X coordinate of the incident location. + */ + x_coordinate_state_plane: z.coerce.number().optional(), + + /** + * Geo validated, Y coordinate of the incident location. + */ + y_coordinate_state_plane: z.coerce.number().optional(), + + /** + * If the incident location is a Parks Dept facility, the Name of the + * facility will appear here + */ + park_facility_name: z.string(), + + /** + * The borough of incident if it is a Parks Dept facility + */ + park_borough: z.string(), + + /** + * If the incident is a taxi, this field describes the type of TLC + * vehicle. + */ + vehicle_type: z.string().optional(), + + /** + * If the incident is identified as a taxi, this field will display the + * borough of the taxi company. + */ + taxi_company_borough: z.string().optional(), + + /** + * If the incident is identified as a taxi, this field displays the taxi + * pick up location + */ + taxi_pick_up_location: z.string().optional(), + + /** + * If the incident is identified as a Bridge/Highway, the name will be + * displayed here. + */ + bridge_highway_name: z.string().optional(), + + /** + * If the incident is identified as a Bridge/Highway, the direction where + * the issue took place would be displayed here. + */ + bridge_highway_direction: z.string().optional(), + + /** + * If the incident location was Bridge/Highway this column differentiates + * if the issue was on the Road or the Ramp. + */ + road_ramp: z.string().optional(), + + /** + * Additional information on the section of the Bridge/Highway were the + * incident took place. + */ + bridge_highway_segment: z.string().optional(), + + /** + * Geo based Lat of the incident location + */ + latitude: z.coerce.number().optional(), + + /** + * Geo based Long of the incident location + */ + longitude: z.coerce.number().optional(), + + /** + * Combination of the geo based lat & long of the incident location + */ + location: z + .object({ + latitude: z.coerce.number(), + longitude: z.coerce.number(), + needs_recoding: z.boolean(), + human_address: coerceObject({ + address: z.string().optional(), + city: z.string().optional(), + state: z.string().optional(), + zip: z.coerce.number().optional(), + }), + }) + .strict() + .optional(), + + // Unknown Keys - keys that are not in the docs but are in the data + ':@computed_region_sbqj_enih': z.coerce.number().optional(), + ':@computed_region_efsh_h5xi': z.coerce.number().optional(), + ':@computed_region_92fq_4b7q': z.coerce.number().optional(), + ':@computed_region_yeji_bk3q': z.coerce.number().optional(), + ':@computed_region_f5dn_yrer': z.coerce.number().optional(), + }) + .strict(); export type Outage = z.infer; diff --git a/types/projects.ts b/types/projects.ts index 68b33b8..269f427 100644 --- a/types/projects.ts +++ b/types/projects.ts @@ -1,315 +1,324 @@ import { z } from 'zod'; -export const ProjectSchema = z.object({ - /** The date the dataset was refreshed */ - data_through_date: z.coerce.date(), - - /** - * Either Maintenance, Non-Tier 1, Tier 1, Tier 2, Tier 4, OREC, or N/A. - * Maintenance represents existing baseline resources. Non-Tier 1 are - * facilities with a commercial operation date before 1/1/2015, and Tier 1 - * are facilities with a commercial operation date after 1/1/2015. Tier 2 - * facilities are from wind and non-state-owned run-of-river hydroelectric - * generating facilities located within New York State that entered - * commercial operation prior to January 1, 2015. OREC represents projects - * in the offshore wind program. Tier 4 represents renewable energy and - * transmission projects. - */ - eligibility: z.string(), - - /** - * Name of project. Project names with an asterisk have the same project - * name as projects awarded under different solicitations - */ - project_name: z.string(), - - /** - * Request for Proposal (RFP) number, or Maintenance Agreement number under - * Renewable Portfolio Standard (RPS) or Tier 2 Maintenance under the - * Renewable Energy Standard (RES) - */ - solicitation_name: z.string(), - - /** - * Certain solicitations provided for an optional or mandatory adjustment to - * the Index REC Strike Price or Fixed REC Price based on changes in - * certain commodity price indices as described in each RFP. Proposers - * awarded under these solicitations whose proposals were subject to this - * adjustment may have their Index REC Strike Price or Fixed REC Price - * adjusted consistent with the methodology described in each applicable RFP. - */ - inflation_adjustment: z.string().optional(), - - /** - * The Fixed REC bid price of Tier 1 and Non-Tier 1 awarded projects in US - * dollars. Blank cells represent data that was not required or is not - * currently available. - */ - fixed_rec_price: z.coerce.number().optional(), - - /** - * The Index OREC bid price or the Index REC Conversion Offer Price of - * Tier 1 or Off-shore wind awarded projects in US dollars. Blank cells - * represent data that was not required or is not currently available. - */ - index_orec_strike_price: z.coerce.number().optional(), - - /** - * Indicates if a Non-Tier 1 or Tier 1 Project claimed Incremental Economic - * Benefits as part of its Bid Proposal, and these claims were accepted by - * NYSERDA as part of its award. Blank cells represent data that was not - * required or is not currently available. For more information, please - * refer to each specific RFP's Section on Incremental Economic Benefits to - * New York State. - */ - incremental_economic_benefits_claimed: z.string().optional(), - - /** - * For Non-Tier 1 or Tier I Projects, indicates the verification status of - * the Incremental Economic Benefits included in the projects Agreement. - * Incremental Economic Benefits are verified following three years of - * operation and must meet a threshold of 85% of claim accepted by NYSERDA. - * Yes - indicates verification complete and project met threshold. No – - * verification complete and project did not meet threshold. Blank cells - * represent – verification not due yet/ not yet complete; no economic - * benefit claim; or project cancelled/terminated. - */ - project_met_economic_benefits_threshold: z.string().optional(), - - /** - * The type of renewable energy technology for the project currently under - * contract or installed; either Land Based Wind, Offshore Wind, - * Hydroelectric, Biomass, Biogas-LFG, Biogas-ADG, Fuel Cell, Solar, - * Geothermal, Maintenance Biomass, or Maintenance Hydroelectric. - */ - renewable_technology: z.string().optional(), - - /** - * Indicates the type of eligible generation, either Existing or New - * Generation. Blank cells represent data that was not required or is not - * currently available. - */ - generation_type: z.string().optional(), - - /** - * Vintage Generation Facility (VGS). VGFs are projects that are either - * upgraded, returned to service, relocated, or repowered, and must meet - * the specified requirements - */ - type_of_existing: z.string().optional(), - - /** - * Name of the Seller in NYSERDA's agreement. A counterparty is the other - * party to a NYSERDA agreement. Blank cells represent data that was not - * required or is not currently available. - */ - counterparty: z.string().optional(), - - /** - * Name of Developer responsible for developing the project. Blank cells - * represent data that was not required or is not currently available. - */ - developer_name: z.string().optional(), - - /** - * Rating of the electric power to be delivered from an energy storage - * project in alternating current (AC) power. Blank cells represent data - * that was not required or is not currently available. - */ - energy_storage_power_capacity_mwac: z.coerce.number().optional(), - - /** - * Usable installed energy storage capacity measured in alternating current - * (AC) power. It is equal to the total capacity measured during a complete - * discharge from a 100% usable state of charge, performed in accordance - * with the storage manufacturer's specifications, on the commercial - * operation date. Blank cells represent data that was not required or is - * not currently available. This category does not include pumped storage. - */ - energy_storage_energy_capacity_mwh: z.coerce.number().optional(), - - /** - * Indicates the New York Control Area Load Zone into which the Bid - * Facility will interconnect or deliver. Blank cells represent data that - * was not required or is not currently available. - */ - nyiso_zone: z.string().optional(), - - /** - * Indicates the NYISO Generator Point Identifier. A Point Identifier is a - * resource-specific numerical identifier used by the NYISO's software - * systems to identify Generators and other Suppliers and only applies to - * Operating Projects. Blank cells represent data that was not required or - * is not currently available. - */ - ptid: z.string().optional(), - - /** - * For the NYISO Interconnection Queue, indicates the number on the list of - * transmission and generation projects seeking to join the grid. - * Additional information is located at: - * https://www.nyiso.com/interconnections. Tier 4 projects include both - * Resources and Transmission numbers. - */ - interconnection_queue_number: z.string().optional(), - - /** - * The applicable federal, state, or local permitting requirements that the - * Bid Facility is subject to. Blank cells represent data that was not - * required or is not currently available. - */ - permit_process: z.string().optional(), - - /** - * For Offshore Wind and Tier 4 projects, the Federal Permitting Mechanism - * related to Environmental Review and Permitting. For Tier 1 projects, the - * Name of Article 10 or Article VIII Case Number. Blank cells represent - * data that were not required, applicable or are not currently available - */ - regulatory_permitting: z - .object({ - url: z.string(), - }) - .optional(), - - /** Name of Article VII pertaining to Offshore Wind Project or Tier 4 Project. */ - article_vii: z - .object({ - url: z.string(), - }) - .optional(), - - /** - * ZIP code of project. Blank cells represent data that was not required or - * is not currently available. - */ - zip_code: z.string().optional(), - - /** - * Name of US county or Canadian Province for project. Blank cells - * represent data that was not required or is not currently available. - */ - county_province: z.string().optional(), - - /** - * Name of US state or Canadian Province for project. Blank cells represent - * data that was not required or is not currently available. - */ - state_province: z.string().optional(), - - /** - * Regional Economic Development Council of project. In 2011, 10 Regional - * Councils were established and charged with developing long-term - * strategic plans for economic growth in their regions. Blank cells - * indicate the project is not located in New York State. - */ - redc: z.string().optional(), - - /** - * The phase/status of which the project is in as of the Data Through - * Date; either Cancelled, Completed, Under Development, or Operational. A - * status of Cancelled means that NYSERDA's award or contract with the - * counterparty was cancelled or terminated. A status of Completed means - * the project has fulfilled their contractual obligation to NYSERDA. A - * status of Under Development means that the project has not yet entered - * operation. A status of Operational means that the project has entered - * operation. - */ - project_status: z.string(), - - /** - * Date declared as the Commercial Operation date of facility by NYISO or - * local utility. Blank cells represent data that was not required or is - * not currently available. - */ - year_of_commercial_operation: z.string().optional(), - - /** - * Date NYSERDA's payments started or are expected to start. For projects - * Under Development, the dates are reflective of the current status of - * NYSERDA agreements and are subject to change based on project - * development progress. Blank cells represent data that was not required - * or is not currently available. - */ - year_of_delivery_start_date: z.string().optional(), - - /** Number of years of performance under the NYSERDA Agreement. */ - contract_duration: z.coerce.number().optional(), - - /** - * For a new facility, the New Renewable Capacity equals the nameplate - * capacity of the installed equipment. For a repowered facility, End of - * Useful Life Bid Capacity is reported. For an upgraded facility, only the - * renewable capacity resulting from the upgrade of a facility is provided. - * For a single facility that was awarded multiple contracts, each for a - * percentage of the facility's output under multiple RFPs, the total New - * Renewable Capacity for the facility is listed once to avoid - * over-counting. Therefore, the same facility appearing in multiple - * instances may show a New Renewable Capacity of zero. Blank cells - * represent projects with expired contracts with NYSERDA. - */ - new_renewable_capacity_mw: z.coerce.number().optional(), - - /** - * The NYSERDA-contracted portion of the New Renewable Capacity in - * Megawatts. Blank cells represent data that were not applicable, - * required or are not currently available. - */ - bid_capacity_mw: z.coerce.number().optional(), - - /** - * The annual production of the project in Megawatt hours that NYSERDA and - * the Counterparty agree on constitutes performance in an Agreement. The - * Bid Quantity may not constitute the entire output of a project. Blank - * cells represent data that was not required or is not currently - * available. - */ - bid_quantity_mwh: z.coerce.number().optional(), - - /** - * The maximum annual contractual obligation of the project in Megawatt - * hours. Blank cells represent data that was not required or is not - * currently available. - */ - max_annual_contract_quantity: z.coerce.number().optional(), - - /** - * An amount of electrical energy (in MWh), such that the estimated - * probability in any given year that generation from the Selected Project - * delivered to the Delivery Point would exceed that amount is 10 percent. - * Applicable to Offshore Wind projects only. Blank cells represent data - * that was not required or is not currently available. - */ - p10_annual_orec_exceedance: z.coerce.number().optional(), - - /** - * A NYSERDA calculation based on a 10.5 meter/second average Hub Height - * (m/s avgHH) wind speed. Weibull parameters of A = 11.3 and K = 2.2 were - * used to create an annual wind frequency distribution table. The - * resulting frequency distribution was then applied in the NREL 15 MW - * turbine power curve assuming 22% losses. Blank cells represent data - * there were not required or is not currently available. - */ - p50_generation_calculated_by_nyserda_mwh_yr: z.coerce.number().optional(), - - /** - * Capacity of a project's new HVDC transmission line. Applicable to Tier 4 - * projects only. Blank cells represent data that was not required or is - * not currently available. - */ - transmission_capacity_hvdc: z.coerce.number().optional(), - - /** - * Open Data/Socrata-generated geocoding information based on supplied - * address components. - */ - georeference: z - .object({ - coordinates: z.tuple([z.number(), z.number()]), - type: z.literal('Point'), - }) - .optional(), -}); +export const ProjectSchema = z + .object({ + /** The date the dataset was refreshed */ + data_through_date: z.coerce.date(), + + /** + * Either Maintenance, Non-Tier 1, Tier 1, Tier 2, Tier 4, OREC, or N/A. + * Maintenance represents existing baseline resources. Non-Tier 1 are + * facilities with a commercial operation date before 1/1/2015, and Tier 1 + * are facilities with a commercial operation date after 1/1/2015. Tier 2 + * facilities are from wind and non-state-owned run-of-river hydroelectric + * generating facilities located within New York State that entered + * commercial operation prior to January 1, 2015. OREC represents projects + * in the offshore wind program. Tier 4 represents renewable energy and + * transmission projects. + */ + eligibility: z.string(), + + /** + * Name of project. Project names with an asterisk have the same project + * name as projects awarded under different solicitations + */ + project_name: z.string(), + + /** + * Request for Proposal (RFP) number, or Maintenance Agreement number under + * Renewable Portfolio Standard (RPS) or Tier 2 Maintenance under the + * Renewable Energy Standard (RES) + */ + solicitation_name: z.string(), + + /** + * Certain solicitations provided for an optional or mandatory adjustment to + * the Index REC Strike Price or Fixed REC Price based on changes in + * certain commodity price indices as described in each RFP. Proposers + * awarded under these solicitations whose proposals were subject to this + * adjustment may have their Index REC Strike Price or Fixed REC Price + * adjusted consistent with the methodology described in each applicable RFP. + */ + inflation_adjustment: z.string().optional(), + + /** + * The Fixed REC bid price of Tier 1 and Non-Tier 1 awarded projects in US + * dollars. Blank cells represent data that was not required or is not + * currently available. + */ + fixed_rec_price: z.coerce.number().optional(), + + /** + * The Index OREC bid price or the Index REC Conversion Offer Price of + * Tier 1 or Off-shore wind awarded projects in US dollars. Blank cells + * represent data that was not required or is not currently available. + */ + index_orec_strike_price: z.coerce.number().optional(), + + /** + * Indicates if a Non-Tier 1 or Tier 1 Project claimed Incremental Economic + * Benefits as part of its Bid Proposal, and these claims were accepted by + * NYSERDA as part of its award. Blank cells represent data that was not + * required or is not currently available. For more information, please + * refer to each specific RFP's Section on Incremental Economic Benefits to + * New York State. + */ + incremental_economic_benefits_claimed: z.string().optional(), + + /** + * For Non-Tier 1 or Tier I Projects, indicates the verification status of + * the Incremental Economic Benefits included in the projects Agreement. + * Incremental Economic Benefits are verified following three years of + * operation and must meet a threshold of 85% of claim accepted by NYSERDA. + * Yes - indicates verification complete and project met threshold. No – + * verification complete and project did not meet threshold. Blank cells + * represent – verification not due yet/ not yet complete; no economic + * benefit claim; or project cancelled/terminated. + */ + project_met_economic_benefits_threshold: z.string().optional(), + + /** + * The type of renewable energy technology for the project currently under + * contract or installed; either Land Based Wind, Offshore Wind, + * Hydroelectric, Biomass, Biogas-LFG, Biogas-ADG, Fuel Cell, Solar, + * Geothermal, Maintenance Biomass, or Maintenance Hydroelectric. + */ + renewable_technology: z.string().optional(), + + /** + * Indicates the type of eligible generation, either Existing or New + * Generation. Blank cells represent data that was not required or is not + * currently available. + */ + generation_type: z.string().optional(), + + /** + * Vintage Generation Facility (VGS). VGFs are projects that are either + * upgraded, returned to service, relocated, or repowered, and must meet + * the specified requirements + */ + type_of_existing: z.string().optional(), + + /** + * Name of the Seller in NYSERDA's agreement. A counterparty is the other + * party to a NYSERDA agreement. Blank cells represent data that was not + * required or is not currently available. + */ + counterparty: z.string().optional(), + + /** + * Name of Developer responsible for developing the project. Blank cells + * represent data that was not required or is not currently available. + */ + developer_name: z.string().optional(), + + /** + * Rating of the electric power to be delivered from an energy storage + * project in alternating current (AC) power. Blank cells represent data + * that was not required or is not currently available. + */ + energy_storage_power_capacity_mwac: z.coerce.number().optional(), + + /** + * Usable installed energy storage capacity measured in alternating current + * (AC) power. It is equal to the total capacity measured during a complete + * discharge from a 100% usable state of charge, performed in accordance + * with the storage manufacturer's specifications, on the commercial + * operation date. Blank cells represent data that was not required or is + * not currently available. This category does not include pumped storage. + */ + energy_storage_energy_capacity_mwh: z.coerce.number().optional(), + + /** + * Indicates the New York Control Area Load Zone into which the Bid + * Facility will interconnect or deliver. Blank cells represent data that + * was not required or is not currently available. + */ + nyiso_zone: z.string().optional(), + + /** + * Indicates the NYISO Generator Point Identifier. A Point Identifier is a + * resource-specific numerical identifier used by the NYISO's software + * systems to identify Generators and other Suppliers and only applies to + * Operating Projects. Blank cells represent data that was not required or + * is not currently available. + */ + ptid: z.string().optional(), + + /** + * For the NYISO Interconnection Queue, indicates the number on the list of + * transmission and generation projects seeking to join the grid. + * Additional information is located at: + * https://www.nyiso.com/interconnections. Tier 4 projects include both + * Resources and Transmission numbers. + */ + interconnection_queue_number: z.string().optional(), + + /** + * The applicable federal, state, or local permitting requirements that the + * Bid Facility is subject to. Blank cells represent data that was not + * required or is not currently available. + */ + permit_process: z.string().optional(), + + /** + * For Offshore Wind and Tier 4 projects, the Federal Permitting Mechanism + * related to Environmental Review and Permitting. For Tier 1 projects, the + * Name of Article 10 or Article VIII Case Number. Blank cells represent + * data that were not required, applicable or are not currently available + */ + regulatory_permitting: z + .object({ + url: z.string(), + }) + .strict() + .optional(), + + /** Name of Article VII pertaining to Offshore Wind Project or Tier 4 Project. */ + article_vii: z + .object({ + url: z.string(), + }) + .strict() + .optional(), + + /** + * ZIP code of project. Blank cells represent data that was not required or + * is not currently available. + */ + zip_code: z.string().optional(), + + /** + * Name of US county or Canadian Province for project. Blank cells + * represent data that was not required or is not currently available. + */ + county_province: z.string().optional(), + + /** + * Name of US state or Canadian Province for project. Blank cells represent + * data that was not required or is not currently available. + */ + state_province: z.string().optional(), + + /** + * Regional Economic Development Council of project. In 2011, 10 Regional + * Councils were established and charged with developing long-term + * strategic plans for economic growth in their regions. Blank cells + * indicate the project is not located in New York State. + */ + redc: z.string().optional(), + + /** + * The phase/status of which the project is in as of the Data Through + * Date; either Cancelled, Completed, Under Development, or Operational. A + * status of Cancelled means that NYSERDA's award or contract with the + * counterparty was cancelled or terminated. A status of Completed means + * the project has fulfilled their contractual obligation to NYSERDA. A + * status of Under Development means that the project has not yet entered + * operation. A status of Operational means that the project has entered + * operation. + */ + project_status: z.string(), + + /** + * Date declared as the Commercial Operation date of facility by NYISO or + * local utility. Blank cells represent data that was not required or is + * not currently available. + */ + year_of_commercial_operation: z.string().optional(), + + /** + * Date NYSERDA's payments started or are expected to start. For projects + * Under Development, the dates are reflective of the current status of + * NYSERDA agreements and are subject to change based on project + * development progress. Blank cells represent data that was not required + * or is not currently available. + */ + year_of_delivery_start_date: z.string().optional(), + + /** Number of years of performance under the NYSERDA Agreement. */ + contract_duration: z.coerce.number().optional(), + + /** + * For a new facility, the New Renewable Capacity equals the nameplate + * capacity of the installed equipment. For a repowered facility, End of + * Useful Life Bid Capacity is reported. For an upgraded facility, only the + * renewable capacity resulting from the upgrade of a facility is provided. + * For a single facility that was awarded multiple contracts, each for a + * percentage of the facility's output under multiple RFPs, the total New + * Renewable Capacity for the facility is listed once to avoid + * over-counting. Therefore, the same facility appearing in multiple + * instances may show a New Renewable Capacity of zero. Blank cells + * represent projects with expired contracts with NYSERDA. + */ + new_renewable_capacity_mw: z.coerce.number().optional(), + + /** + * The NYSERDA-contracted portion of the New Renewable Capacity in + * Megawatts. Blank cells represent data that were not applicable, + * required or are not currently available. + */ + bid_capacity_mw: z.coerce.number().optional(), + + /** + * The annual production of the project in Megawatt hours that NYSERDA and + * the Counterparty agree on constitutes performance in an Agreement. The + * Bid Quantity may not constitute the entire output of a project. Blank + * cells represent data that was not required or is not currently + * available. + */ + bid_quantity_mwh: z.coerce.number().optional(), + + /** + * The maximum annual contractual obligation of the project in Megawatt + * hours. Blank cells represent data that was not required or is not + * currently available. + */ + max_annual_contract_quantity: z.coerce.number().optional(), + + /** + * An amount of electrical energy (in MWh), such that the estimated + * probability in any given year that generation from the Selected Project + * delivered to the Delivery Point would exceed that amount is 10 percent. + * Applicable to Offshore Wind projects only. Blank cells represent data + * that was not required or is not currently available. + */ + p10_annual_orec_exceedance: z.coerce.number().optional(), + + /** + * A NYSERDA calculation based on a 10.5 meter/second average Hub Height + * (m/s avgHH) wind speed. Weibull parameters of A = 11.3 and K = 2.2 were + * used to create an annual wind frequency distribution table. The + * resulting frequency distribution was then applied in the NREL 15 MW + * turbine power curve assuming 22% losses. Blank cells represent data + * there were not required or is not currently available. + */ + p50_generation_calculated_by_nyserda_mwh_yr: z.coerce.number().optional(), + + /** + * Capacity of a project's new HVDC transmission line. Applicable to Tier 4 + * projects only. Blank cells represent data that was not required or is + * not currently available. + */ + transmission_capacity_hvdc: z.coerce.number().optional(), + + /** + * Open Data/Socrata-generated geocoding information based on supplied + * address components. + */ + georeference: z + .object({ + coordinates: z.tuple([z.number(), z.number()]), + type: z.literal('Point'), + }) + .strict() + .optional(), + + // Unknown Keys - keys that are not in the docs but are in the data + transmission_capacity_hvdc_: z.coerce.number().optional(), + p50_generation_calculated_by_nyserda_mwh_yr_: z.coerce.number().optional(), + }) + .strict(); export type Project = z.infer; diff --git a/types/ratSightings.ts b/types/ratSightings.ts index 5813769..878f40d 100644 --- a/types/ratSightings.ts +++ b/types/ratSightings.ts @@ -1,221 +1,233 @@ import { z } from 'zod'; -export const RatSightingSchema = z.object({ - /** - * Unique identifier of a Service Request (SR) in the open data set - */ - unique_key: z.coerce.number(), - - /** - * Date SR was created - */ - created_date: z.coerce.date(), - - /** - * Date SR was closed by responding agency - */ - closed_date: z.coerce.date().optional(), - - /** - * Acronym of responding City Government Agency - */ - agency: z.string(), - - /** - * Full Agency name of responding City Government Agency - */ - agency_name: z.string(), - - /** - * This is the first level of a hierarchy identifying the topic of the - * incident or condition. Complaint Type may have a corresponding - * Descriptor (below) or may stand alone. - */ - complaint_type: z.string(), - - /** - * This is associated to the Complaint Type, and provides further detail - * on the incident or condition. Descriptor values are dependent on the - * Complaint Type, and are not always required in SR. - */ - descriptor: z.string(), - - /** - * Describes the type of location used in the address information - */ - location_type: z.string().optional(), - - /** - * Incident location zip code, provided by geo validation. - */ - // Handle extremely rare edge case, where the string 'N/A' is returned - incident_zip: z.preprocess( - (val) => (val === 'N/A' ? undefined : val), - z.coerce.number().optional() - ), - - /** - * House number of incident address provided by submitter. - */ - incident_address: z.string().optional(), - - /** - * Street name of incident address provided by the submitter - */ - street_name: z.string().optional(), - - /** - * First Cross street based on the geo validated incident location - */ - cross_street_1: z.string().optional(), - - /** - * Second Cross Street based on the geo validated incident location - */ - cross_street_2: z.string().optional(), - - /** - * First intersecting street based on geo validated incident location - */ - intersection_street_1: z.string().optional(), - - /** - * Second intersecting street based on geo validated incident location - */ - intersection_street_2: z.string().optional(), - - /** - * Type of incident location information available. - */ - address_type: z.string().optional(), - - /** - * City of the incident location provided by geovalidation. - */ - city: z.string().optional(), - - /** - * If the incident location is identified as a Landmark the name of the - * landmark will display here - */ - landmark: z.string().optional(), - - /** - * If available, this field describes the type of city facility - * associated to the SR - */ - facility_type: z.string().optional(), - - /** - * Status of SR submitted - */ - status: z.string(), - - /** - * Date when responding agency is expected to update the SR. This is - * based on the Complaint Type and internal Service Level Agreements - * (SLAs). - */ - due_date: z.coerce.date().optional(), - - /** - * Date when responding agency last updated the SR. - */ - resolution_action_updated_date: z.coerce.date().optional(), - - /** - * Provided by geovalidation. - */ - community_board: z.string().optional(), - - /** - * Provided by the submitter and confirmed by geovalidation. - */ - borough: z.string().optional(), - - /** - * Geo validated, X coordinate of the incident location. - */ - x_coordinate_state_plane: z.coerce.number().optional(), - - /** - * Geo validated, Y coordinate of the incident location. - */ - y_coordinate_state_plane: z.coerce.number().optional(), - - /** - * If the incident location is a Parks Dept facility, the Name of the - * facility will appear here - */ - park_facility_name: z.string(), - - /** - * The borough of incident if it is a Parks Dept facility - */ - park_borough: z.string().optional(), - - /** - * If the incident is a taxi, this field describes the type of TLC - * vehicle. - */ - vehicle_type: z.string().optional(), - - /** - * If the incident is identified as a taxi, this field will display the - * borough of the taxi company. - */ - taxi_company_borough: z.string().optional(), - - /** - * If the incident is identified as a taxi, this field displays the taxi - * pick up location - */ - taxi_pick_up_location: z.string().optional(), - - /** - * If the incident is identified as a Bridge/Highway, the name will be - * displayed here. - */ - bridge_highway_name: z.string().optional(), - - /** - * If the incident is identified as a Bridge/Highway, the direction where - * the issue took place would be displayed here. - */ - bridge_highway_direction: z.string().optional(), - - /** - * If the incident location was Bridge/Highway this column differentiates - * if the issue was on the Road or the Ramp. - */ - road_ramp: z.string().optional(), - - /** - * Additional information on the section of the Bridge/Highway were the - * incident took place. - */ - bridge_highway_segment: z.string().optional(), - - /** - * Geo based Lat of the incident location - */ - latitude: z.coerce.number().optional(), - - /** - * Geo based Long of the incident location - */ - longitude: z.coerce.number().optional(), - - /** - * Combination of the geo based lat & long of the incident location - */ - location: z - .object({ - coordinates: z.tuple([z.number(), z.number()]), - type: z.literal('Point'), - }) - .optional(), -}); +export const RatSightingSchema = z + .object({ + /** + * Unique identifier of a Service Request (SR) in the open data set + */ + unique_key: z.coerce.number(), + + /** + * Date SR was created + */ + created_date: z.coerce.date(), + + /** + * Date SR was closed by responding agency + */ + closed_date: z.coerce.date().optional(), + + /** + * Acronym of responding City Government Agency + */ + agency: z.string(), + + /** + * Full Agency name of responding City Government Agency + */ + agency_name: z.string(), + + /** + * This is the first level of a hierarchy identifying the topic of the + * incident or condition. Complaint Type may have a corresponding + * Descriptor (below) or may stand alone. + */ + complaint_type: z.string(), + + /** + * This is associated to the Complaint Type, and provides further detail + * on the incident or condition. Descriptor values are dependent on the + * Complaint Type, and are not always required in SR. + */ + descriptor: z.string(), + + /** + * Describes the type of location used in the address information + */ + location_type: z.string().optional(), + + /** + * Incident location zip code, provided by geo validation. + */ + // Handle extremely rare edge case, where the string 'N/A' is returned + incident_zip: z.preprocess( + (val) => (val === 'N/A' ? undefined : val), + z.coerce.number().optional() + ), + + /** + * House number of incident address provided by submitter. + */ + incident_address: z.string().optional(), + + /** + * Street name of incident address provided by the submitter + */ + street_name: z.string().optional(), + + /** + * First Cross street based on the geo validated incident location + */ + cross_street_1: z.string().optional(), + + /** + * Second Cross Street based on the geo validated incident location + */ + cross_street_2: z.string().optional(), + + /** + * First intersecting street based on geo validated incident location + */ + intersection_street_1: z.string().optional(), + + /** + * Second intersecting street based on geo validated incident location + */ + intersection_street_2: z.string().optional(), + + /** + * Type of incident location information available. + */ + address_type: z.string().optional(), + + /** + * City of the incident location provided by geovalidation. + */ + city: z.string().optional(), + + /** + * If the incident location is identified as a Landmark the name of the + * landmark will display here + */ + landmark: z.string().optional(), + + /** + * If available, this field describes the type of city facility + * associated to the SR + */ + facility_type: z.string().optional(), + + /** + * Status of SR submitted + */ + status: z.string(), + + /** + * Date when responding agency is expected to update the SR. This is + * based on the Complaint Type and internal Service Level Agreements + * (SLAs). + */ + due_date: z.coerce.date().optional(), + + /** + * Date when responding agency last updated the SR. + */ + resolution_action_updated_date: z.coerce.date().optional(), + + /** + * Provided by geovalidation. + */ + community_board: z.string().optional(), + + /** + * Provided by the submitter and confirmed by geovalidation. + */ + borough: z.string().optional(), + + /** + * Geo validated, X coordinate of the incident location. + */ + x_coordinate_state_plane: z.coerce.number().optional(), + + /** + * Geo validated, Y coordinate of the incident location. + */ + y_coordinate_state_plane: z.coerce.number().optional(), + + /** + * If the incident location is a Parks Dept facility, the Name of the + * facility will appear here + */ + park_facility_name: z.string(), + + /** + * The borough of incident if it is a Parks Dept facility + */ + park_borough: z.string().optional(), + + /** + * If the incident is a taxi, this field describes the type of TLC + * vehicle. + */ + vehicle_type: z.string().optional(), + + /** + * If the incident is identified as a taxi, this field will display the + * borough of the taxi company. + */ + taxi_company_borough: z.string().optional(), + + /** + * If the incident is identified as a taxi, this field displays the taxi + * pick up location + */ + taxi_pick_up_location: z.string().optional(), + + /** + * If the incident is identified as a Bridge/Highway, the name will be + * displayed here. + */ + bridge_highway_name: z.string().optional(), + + /** + * If the incident is identified as a Bridge/Highway, the direction where + * the issue took place would be displayed here. + */ + bridge_highway_direction: z.string().optional(), + + /** + * If the incident location was Bridge/Highway this column differentiates + * if the issue was on the Road or the Ramp. + */ + road_ramp: z.string().optional(), + + /** + * Additional information on the section of the Bridge/Highway were the + * incident took place. + */ + bridge_highway_segment: z.string().optional(), + + /** + * Geo based Lat of the incident location + */ + latitude: z.coerce.number().optional(), + + /** + * Geo based Long of the incident location + */ + longitude: z.coerce.number().optional(), + + /** + * Combination of the geo based lat & long of the incident location + */ + location: z + .object({ + coordinates: z.tuple([z.number(), z.number()]), + type: z.literal('Point'), + }) + .strict() + .optional(), + + // Unknown Keys - keys that are not in the docs but are in the data + x_coordinate_state_plane_: z.coerce.number().optional(), + y_coordinate_state_plane_: z.coerce.number().optional(), + ':@computed_region_efsh_h5xi': z.coerce.number().optional(), + ':@computed_region_f5dn_yrer': z.coerce.number().optional(), + ':@computed_region_yeji_bk3q': z.coerce.number().optional(), + ':@computed_region_92fq_4b7q': z.coerce.number().optional(), + ':@computed_region_sbqj_enih': z.coerce.number().optional(), + }) + .strict(); export type RatSighting = z.infer; From cc73a40aa35c58da9ce2b0c88ca5f0397fca14f5 Mon Sep 17 00:00:00 2001 From: KaBankz <20971850+KaBankz@users.noreply.github.com> Date: Mon, 20 Jan 2025 21:22:56 -0500 Subject: [PATCH 06/14] Uncomment env variable validation --- app/maps/actions.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/maps/actions.ts b/app/maps/actions.ts index 797ef44..03a9b01 100644 --- a/app/maps/actions.ts +++ b/app/maps/actions.ts @@ -5,11 +5,11 @@ import { OutagesSchema } from '@/types/outages'; import { ProjectsSchema } from '@/types/projects'; import { RatSightingsSchema } from '@/types/ratSightings'; -const SOCRATA_APP_TOKEN = ''; +const SOCRATA_APP_TOKEN = process.env.SOCRATA_APP_TOKEN; -// if (!SOCRATA_APP_TOKEN) { -// throw new Error('SOCRATA_APP_TOKEN environment variable is not set'); -// } +if (!SOCRATA_APP_TOKEN) { + throw new Error('SOCRATA_APP_TOKEN is not set'); +} export async function getProjects({ limit = 1000, From dd4cfa74245bb9bb86e0b9f853e7af156c0f29e1 Mon Sep 17 00:00:00 2001 From: KaBankz <20971850+KaBankz@users.noreply.github.com> Date: Mon, 20 Jan 2025 23:13:27 -0500 Subject: [PATCH 07/14] Refactor fetch calls to reduce duplicated code --- app/maps/actions.ts | 237 +++++++++++++++++++++----------------------- 1 file changed, 111 insertions(+), 126 deletions(-) diff --git a/app/maps/actions.ts b/app/maps/actions.ts index 03a9b01..9d9cef6 100644 --- a/app/maps/actions.ts +++ b/app/maps/actions.ts @@ -1,173 +1,158 @@ 'use server'; +import { z } from 'zod'; + import { EVChargingStationsSchema } from '@/types/evChargingStations'; import { OutagesSchema } from '@/types/outages'; import { ProjectsSchema } from '@/types/projects'; import { RatSightingsSchema } from '@/types/ratSightings'; -const SOCRATA_APP_TOKEN = process.env.SOCRATA_APP_TOKEN; +const SOCRATA_APP_TOKEN = process.env.SOCRATA_APP_TOKEN!; if (!SOCRATA_APP_TOKEN) { throw new Error('SOCRATA_APP_TOKEN is not set'); } -export async function getProjects({ - limit = 1000, - offset = 0, -}: { +type SocrataQueryParams = { + /** + * The set of columns to be returned, similar to a SELECT in SQL + * + * @default All columns, equivalent to $select=* + */ + select?: string; + /** + * Filters the rows to be returned, similar to a WHERE in SQL + * + * @default No filter + */ + where?: string; + /** + * Column to order results on, similar to a ORDER BY in SQL + * + * @default Unspecified order + */ + order?: string; + /** + * Column to group results on, similar to a GROUP BY in SQL + * + * @default No grouping + */ + group?: string; + /** + * Filters the rows that result from an aggregation, similar to HAVING + * + * @default No filter + */ + having?: string; /** - * The number of results to return + * Maximum number of results to return * * @default 1000 */ limit?: number; /** - * The index of the result array where to start the returned list of results. + * Offset count into the results to start at, used for paging * * @default 0 **/ offset?: number; -}) { - try { - const response = await fetch( - `https://data.ny.gov/resource/dprp-55ye.json?$limit=${limit}&$offset=${offset}`, - { - headers: { - 'X-App-Token': SOCRATA_APP_TOKEN, - }, - } - ); + /** + * Performs a full text search for a value. + * + * @default No search + */ + q?: string; +}; - if (!response.ok) { - throw new Error('API Error'); - } +async function fetchSocrataData({ + endpoint, + schema, + params, +}: { + endpoint: string; + schema: z.ZodType; + params: SocrataQueryParams; +}): Promise { + const { + limit = 1000, + offset = 0, + select, + where, + order, + group, + having, + q, + } = params; + + const queryParams = new URLSearchParams({ + ...(select && { $select: select }), + ...(where && { $where: where }), + ...(order && { $order: order }), + ...(group && { $group: group }), + ...(having && { $having: having }), + ...(q && { $q: q }), + $limit: limit.toString(), + $offset: offset.toString(), + }); + + const response = await fetch(`${endpoint}?${queryParams.toString()}`, { + headers: { + 'X-App-Token': SOCRATA_APP_TOKEN, + }, + }); + + if (!response.ok) { + throw new Error('API Error'); + } - const data = await response.json(); + const data = await response.json(); + return schema.parse(data); +} - const projects = ProjectsSchema.parse(data); - return projects; +export async function getProjects(params: SocrataQueryParams = {}) { + try { + return await fetchSocrataData({ + endpoint: 'https://data.ny.gov/resource/dprp-55ye.json', + schema: ProjectsSchema, + params, + }); } catch (error) { console.error('Failed to fetch projects:', error); - throw error; } } -export async function getOutages({ - limit = 1000, - offset = 0, -}: { - /** - * The number of results to return - * - * @default 1000 - */ - limit?: number; - /** - * The index of the result array where to start the returned list of results. - * - * @default 0 - **/ - offset?: number; -}) { +export async function getOutages(params: SocrataQueryParams = {}) { try { - const response = await fetch( - `https://data.cityofnewyork.us/resource/br6j-yp22.json?$limit=${limit}&$offset=${offset}`, - { - headers: { - 'X-App-Token': SOCRATA_APP_TOKEN, - }, - } - ); - - if (!response.ok) { - throw new Error('API Error'); - } - - const data = await response.json(); - const outages = OutagesSchema.parse(data); - return outages; + return await fetchSocrataData({ + endpoint: 'https://data.cityofnewyork.us/resource/br6j-yp22.json', + schema: OutagesSchema, + params, + }); } catch (error) { console.error('Failed to fetch outages:', error); - throw error; } } -export async function getEVChargingStations({ - limit = 1000, - offset = 0, -}: { - /** - * The number of results to return - * - * @default 1000 - */ - limit?: number; - /** - * The index of the result array where to start the returned list of results. - * - * @default 0 - **/ - offset?: number; -}) { +export async function getEVChargingStations(params: SocrataQueryParams = {}) { try { - const response = await fetch( - `https://data.cityofnewyork.us/resource/fc53-9hrv.json?$limit=${limit}&$offset=${offset}`, - { - headers: { - 'X-App-Token': SOCRATA_APP_TOKEN, - }, - } - ); - - if (!response.ok) { - throw new Error('API Error'); - } - - const data = await response.json(); - const evChargingStations = EVChargingStationsSchema.parse(data); - return evChargingStations; + return await fetchSocrataData({ + endpoint: 'https://data.cityofnewyork.us/resource/fc53-9hrv.json', + schema: EVChargingStationsSchema, + params, + }); } catch (error) { console.error('Failed to fetch ev charging stations:', error); - throw error; } } -export async function getRatSightings({ - limit = 1000, - offset = 0, -}: { - /** - * The number of results to return - * - * @default 1000 - */ - limit?: number; - /** - * The index of the result array where to start the returned list of results. - * - * @default 0 - **/ - offset?: number; -}) { +export async function getRatSightings(params: SocrataQueryParams = {}) { try { - const response = await fetch( - `https://data.cityofnewyork.us/resource/3q43-55fe.json?$limit=${limit}&$offset=${offset}`, - { - headers: { - 'X-App-Token': SOCRATA_APP_TOKEN, - }, - } - ); - - if (!response.ok) { - throw new Error('API Error'); - } - - const data = await response.json(); - const ratSightings = RatSightingsSchema.parse(data); - return ratSightings; + return await fetchSocrataData({ + endpoint: 'https://data.cityofnewyork.us/resource/3q43-55fe.json', + schema: RatSightingsSchema, + params, + }); } catch (error) { console.error('Failed to fetch rat sightings:', error); - throw error; } } From 22c44690756da0c8aff6a03f45dc55b81e514379 Mon Sep 17 00:00:00 2001 From: KaBankz <20971850+KaBankz@users.noreply.github.com> Date: Mon, 20 Jan 2025 23:38:39 -0500 Subject: [PATCH 08/14] Add initial map --- app/maps/page.tsx | 28 +++++++++++++++++++++++++++- package-lock.json | 11 +++++++++++ package.json | 1 + 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/app/maps/page.tsx b/app/maps/page.tsx index c921807..9e9be0d 100644 --- a/app/maps/page.tsx +++ b/app/maps/page.tsx @@ -1,3 +1,29 @@ +'use client'; + +import { useEffect, useRef } from 'react'; + +import mapboxgl from 'mapbox-gl'; + export default function MapsPage() { - return
Maps
; + const mapContainerRef = useRef(null); + const mapRef = useRef(null); + + useEffect(() => { + mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN; + + if (mapContainerRef.current) { + mapRef.current = new mapboxgl.Map({ + container: mapContainerRef.current, + style: 'mapbox://styles/mapbox/dark-v11', + center: [-74.0242, 40.6941], + zoom: 10.12, + }); + } + + return () => { + mapRef.current?.remove(); + }; + }, []); + + return
; } diff --git a/package-lock.json b/package-lock.json index 2984a6e..7a8d772 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "devDependencies": { "@eslint/eslintrc": "^3", "@ianvs/prettier-plugin-sort-imports": "^4.4.1", + "@types/mapbox-gl": "^3.4.1", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", @@ -1166,6 +1167,16 @@ "@types/pbf": "*" } }, + "node_modules/@types/mapbox-gl": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-3.4.1.tgz", + "integrity": "sha512-NsGKKtgW93B+UaLPti6B7NwlxYlES5DpV5Gzj9F75rK5ALKsqSk15CiEHbOnTr09RGbr6ZYiCdI+59NNNcAImg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/node": { "version": "20.17.14", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.14.tgz", diff --git a/package.json b/package.json index fd9f0cc..fd226b3 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "devDependencies": { "@eslint/eslintrc": "^3", "@ianvs/prettier-plugin-sort-imports": "^4.4.1", + "@types/mapbox-gl": "^3.4.1", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", From c40fa7d30922c7029f08fac57dd8f7c6700d70a4 Mon Sep 17 00:00:00 2001 From: KaBankz <20971850+KaBankz@users.noreply.github.com> Date: Tue, 21 Jan 2025 16:00:59 -0500 Subject: [PATCH 09/14] Make schema more granular --- types/evChargingStations.ts | 32 +++++++++++++++++++--- types/outages.ts | 50 +++++++++++++++++++++------------- types/projects.ts | 48 +++++++++++++++++++++++++++------ types/ratSightings.ts | 54 ++++++++++++++++++++++++++++++------- 4 files changed, 146 insertions(+), 38 deletions(-) diff --git a/types/evChargingStations.ts b/types/evChargingStations.ts index b89009a..f69ad77 100644 --- a/types/evChargingStations.ts +++ b/types/evChargingStations.ts @@ -15,7 +15,16 @@ export const EVChargingStationSchema = z /** * The type of charger at the station */ - type_of_charger: z.string(), + type_of_charger: z.enum([ + 'DOT Municipal Level 2 Charger', + 'DOT Municipal Level 3 Charger', + 'EV Solar Arc Charger', + 'EV Solar Canopy Charger', + 'L2 DOT Flo Curbside Charger', + 'Level 2 Charger', + 'Level 3 Fast Charger', + 'Mobile Charger', + ]), /** * The # of ports at the station @@ -44,12 +53,29 @@ export const EVChargingStationSchema = z * Staten Island - Richmond County). NYC's boroughs have existed since the * consolidation of the city in 1898. */ - borough: z.string().optional(), + borough: z + .enum([ + 'Bronx', + 'Brooklyn', + 'Manhattan', + 'Queens', + 'Staten Island', + 'Upstate', + ]) + .optional(), /** * Is the charging station open to the general public? */ - public_charger: z.string().optional(), + public_charger: z + .enum([ + '*adapter required for use', + 'City Hall Charging', + 'NYC DC Fast Public Charging', + 'Solar Carport Charging', + 'Yes', + ]) + .optional(), /** * Is there a fee to use the charger at the station for City drivers? diff --git a/types/outages.ts b/types/outages.ts index 16d5e9c..4b8ae43 100644 --- a/types/outages.ts +++ b/types/outages.ts @@ -32,31 +32,38 @@ export const OutageSchema = z /** * Acronym of responding City Government Agency */ - agency: z.string(), + agency: z.enum(['HPD']), /** * Full Agency name of responding City Government Agency */ - agency_name: z.string(), + agency_name: z.enum([ + 'Department of Housing Preservation and Development', + 'Division of Alternative Management', + ]), /** * This is the first level of a hierarchy identifying the topic of the * incident or condition. Complaint Type may have a corresponding * Descriptor (below) or may stand alone. */ - complaint_type: z.string(), + complaint_type: z.enum(['ELECTRIC', 'Electric']), /** * This is associated to the Complaint Type, and provides further detail on * the incident or condition. Descriptor values are dependent on the * Complaint Type, and are not always required in SR. */ - descriptor: z.string(), + descriptor: z.enum(['POWER OUTAGE', 'Power Outage']), /** * Describes the type of location used in the address information */ - location_type: z.string(), + location_type: z.enum([ + 'Apartment', + 'Building-Wide', + 'RESIDENTIAL BUILDING', + ]), /** * Incident location zip code, provided by geo validation. @@ -96,7 +103,7 @@ export const OutageSchema = z /** * Type of incident location information available. */ - address_type: z.string().optional(), + address_type: z.enum(['ADDRESS']).optional(), /** * City of the incident location provided by geovalidation. @@ -118,7 +125,7 @@ export const OutageSchema = z /** * Status of SR submitted */ - status: z.string(), + status: z.enum(['Closed', 'In Progress', 'Open']), /** * Date when responding agency is expected to update the SR. This is based @@ -139,7 +146,14 @@ export const OutageSchema = z /** * Provided by the submitter and confirmed by geovalidation. */ - borough: z.string(), + borough: z.enum([ + 'BRONX', + 'BROOKLYN', + 'MANHATTAN', + 'QUEENS', + 'STATEN ISLAND', + 'Unspecified', + ]), /** * Geo validated, X coordinate of the incident location. @@ -160,7 +174,14 @@ export const OutageSchema = z /** * The borough of incident if it is a Parks Dept facility */ - park_borough: z.string(), + park_borough: z.enum([ + 'BRONX', + 'BROOKLYN', + 'MANHATTAN', + 'QUEENS', + 'STATEN ISLAND', + 'Unspecified', + ]), /** * If the incident is a taxi, this field describes the type of TLC @@ -219,15 +240,8 @@ export const OutageSchema = z */ location: z .object({ - latitude: z.coerce.number(), - longitude: z.coerce.number(), - needs_recoding: z.boolean(), - human_address: coerceObject({ - address: z.string().optional(), - city: z.string().optional(), - state: z.string().optional(), - zip: z.coerce.number().optional(), - }), + coordinates: z.tuple([z.number(), z.number()]), + type: z.literal('Point'), }) .strict() .optional(), diff --git a/types/projects.ts b/types/projects.ts index 269f427..601261f 100644 --- a/types/projects.ts +++ b/types/projects.ts @@ -16,7 +16,17 @@ export const ProjectSchema = z * in the offshore wind program. Tier 4 represents renewable energy and * transmission projects. */ - eligibility: z.string(), + eligibility: z + .enum([ + 'Maintenance', + 'Non-Tier 1', + 'OREC', + 'Tier 1', + 'Tier 2', + 'Tier 4', + 'N/A', + ]) + .optional(), /** * Name of project. Project names with an asterisk have the same project @@ -39,7 +49,7 @@ export const ProjectSchema = z * adjustment may have their Index REC Strike Price or Fixed REC Price * adjusted consistent with the methodology described in each applicable RFP. */ - inflation_adjustment: z.string().optional(), + inflation_adjustment: z.enum(['Yes']).optional(), /** * The Fixed REC bid price of Tier 1 and Non-Tier 1 awarded projects in US @@ -63,7 +73,7 @@ export const ProjectSchema = z * refer to each specific RFP's Section on Incremental Economic Benefits to * New York State. */ - incremental_economic_benefits_claimed: z.string().optional(), + incremental_economic_benefits_claimed: z.enum(['Yes']).optional(), /** * For Non-Tier 1 or Tier I Projects, indicates the verification status of @@ -75,7 +85,7 @@ export const ProjectSchema = z * represent – verification not due yet/ not yet complete; no economic * benefit claim; or project cancelled/terminated. */ - project_met_economic_benefits_threshold: z.string().optional(), + project_met_economic_benefits_threshold: z.enum(['Yes']).optional(), /** * The type of renewable energy technology for the project currently under @@ -83,21 +93,38 @@ export const ProjectSchema = z * Hydroelectric, Biomass, Biogas-LFG, Biogas-ADG, Fuel Cell, Solar, * Geothermal, Maintenance Biomass, or Maintenance Hydroelectric. */ - renewable_technology: z.string().optional(), + renewable_technology: z + .enum([ + 'Biogas - ADG', + 'Biogas - LFG', + 'Biomass', + 'Fuel Cell', + 'Hydroelectric', + 'Land Based Wind', + 'Land-based Wind', + 'Maintenance Biomass', + 'Maintenance Hydroelectric', + 'Offshore Wind', + 'Solar', + 'Wind/Solar', + ]) + .optional(), /** * Indicates the type of eligible generation, either Existing or New * Generation. Blank cells represent data that was not required or is not * currently available. */ - generation_type: z.string().optional(), + generation_type: z.enum(['Existing', 'New']).optional(), /** * Vintage Generation Facility (VGS). VGFs are projects that are either * upgraded, returned to service, relocated, or repowered, and must meet * the specified requirements */ - type_of_existing: z.string().optional(), + type_of_existing: z + .enum(['Repower', 'Return to Service', 'Upgrade']) + .optional(), /** * Name of the Seller in NYSERDA's agreement. A counterparty is the other @@ -218,7 +245,12 @@ export const ProjectSchema = z * operation. A status of Operational means that the project has entered * operation. */ - project_status: z.string(), + project_status: z.enum([ + 'Cancelled', + 'Completed', + 'Under Development', + 'Operational', + ]), /** * Date declared as the Commercial Operation date of facility by NYISO or diff --git a/types/ratSightings.ts b/types/ratSightings.ts index 878f40d..14dcc29 100644 --- a/types/ratSightings.ts +++ b/types/ratSightings.ts @@ -20,26 +20,26 @@ export const RatSightingSchema = z /** * Acronym of responding City Government Agency */ - agency: z.string(), + agency: z.enum(['DOHMH']), /** * Full Agency name of responding City Government Agency */ - agency_name: z.string(), + agency_name: z.enum(['Department of Health and Mental Hygiene']), /** * This is the first level of a hierarchy identifying the topic of the * incident or condition. Complaint Type may have a corresponding * Descriptor (below) or may stand alone. */ - complaint_type: z.string(), + complaint_type: z.enum(['Rodent']), /** * This is associated to the Complaint Type, and provides further detail * on the incident or condition. Descriptor values are dependent on the * Complaint Type, and are not always required in SR. */ - descriptor: z.string(), + descriptor: z.enum(['Rat Sighting']), /** * Describes the type of location used in the address information @@ -49,7 +49,7 @@ export const RatSightingSchema = z /** * Incident location zip code, provided by geo validation. */ - // Handle extremely rare edge case, where the string 'N/A' is returned + //? Handle extremely rare edge case, where the string 'N/A' is returned incident_zip: z.preprocess( (val) => (val === 'N/A' ? undefined : val), z.coerce.number().optional() @@ -88,7 +88,16 @@ export const RatSightingSchema = z /** * Type of incident location information available. */ - address_type: z.string().optional(), + address_type: z + .enum([ + 'ADDRESS', + 'BLOCKFACE', + 'INTERSECTION', + 'LATLONG', + 'PLACENAME', + 'UNRECOGNIZED', + ]) + .optional(), /** * City of the incident location provided by geovalidation. @@ -110,7 +119,15 @@ export const RatSightingSchema = z /** * Status of SR submitted */ - status: z.string(), + status: z.enum([ + 'Assigned', + 'Closed', + 'Draft', + 'In Progress', + 'Open', + 'Pending', + 'Unspecified', + ]), /** * Date when responding agency is expected to update the SR. This is @@ -132,7 +149,16 @@ export const RatSightingSchema = z /** * Provided by the submitter and confirmed by geovalidation. */ - borough: z.string().optional(), + borough: z + .enum([ + 'BRONX', + 'BROOKLYN', + 'MANHATTAN', + 'QUEENS', + 'STATEN ISLAND', + 'Unspecified', + ]) + .optional(), /** * Geo validated, X coordinate of the incident location. @@ -153,7 +179,17 @@ export const RatSightingSchema = z /** * The borough of incident if it is a Parks Dept facility */ - park_borough: z.string().optional(), + park_borough: z + .enum([ + 'BRONX', + 'BROOKLYN', + 'MANHATTAN', + 'QUEENS', + 'STATEN ISLAND', + 'Unspecified', + '', + ]) + .optional(), /** * If the incident is a taxi, this field describes the type of TLC From 40108b2897de2187b413f58f1f2f86990cdd97e3 Mon Sep 17 00:00:00 2001 From: KaBankz <20971850+KaBankz@users.noreply.github.com> Date: Tue, 21 Jan 2025 19:02:51 -0500 Subject: [PATCH 10/14] Render maps and create custom marker component --- app/layout.tsx | 8 ++- app/maps/page.tsx | 129 ++++++++++++++++++++++++++++++++++++++++++ components/Marker.tsx | 61 ++++++++++++++++++++ package-lock.json | 80 ++++++++++++++++++++++++-- package.json | 5 ++ 5 files changed, 277 insertions(+), 6 deletions(-) create mode 100644 components/Marker.tsx diff --git a/app/layout.tsx b/app/layout.tsx index ac5e6be..a317851 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,7 +1,12 @@ import type { Metadata } from 'next'; import { Geist, Geist_Mono } from 'next/font/google'; -import './globals.css'; +import { config } from '@fortawesome/fontawesome-svg-core'; + +import '@fortawesome/fontawesome-svg-core/styles.css'; +import '@/app/globals.css'; + +config.autoAddCss = false; const geistSans = Geist({ variable: '--font-geist-sans', @@ -38,6 +43,7 @@ export default function RootLayout({ + {/* TODO: Remove this once new maps page is complete */} (null); const mapRef = useRef(null); @@ -18,6 +34,8 @@ export default function MapsPage() { center: [-74.0242, 40.6941], zoom: 10.12, }); + + mapRef.current.addControl(new mapboxgl.NavigationControl()); } return () => { @@ -25,5 +43,116 @@ export default function MapsPage() { }; }, []); + useEffect(() => { + const loadProjects = async () => { + try { + const projects = await getProjects(); + + if (projects) { + projects.forEach((project) => { + if ( + project.georeference?.coordinates[0] && + project.georeference?.coordinates[1] + ) { + new mapboxgl.Marker({ + element: MarkerElement({ icon: faBolt, color: '#ccac00' }), + }) + .setLngLat(project.georeference.coordinates) + .addTo(mapRef.current!); + } + }); + } + } catch (error) { + console.error('Error loading projects:', error); + } + }; + + loadProjects(); + }, []); + + useEffect(() => { + const loadOutages = async () => { + try { + const outages = await getOutages(); + + if (outages) { + outages.forEach((outage) => { + if (outage.latitude && outage.longitude) { + new mapboxgl.Marker({ + element: MarkerElement({ + icon: faPlugCircleXmark, + color: '#ff0000', + }), + }) + .setLngLat([outage.longitude, outage.latitude]) + .addTo(mapRef.current!); + } + }); + } + } catch (error) { + console.error('Error loading outages:', error); + } + }; + + loadOutages(); + }, []); + + useEffect(() => { + const loadEVChargingStations = async () => { + try { + const evChargingStations = await getEVChargingStations(); + + if (evChargingStations) { + evChargingStations.forEach((evChargingStation) => { + if (evChargingStation.latitude && evChargingStation.longitude) { + new mapboxgl.Marker({ + element: MarkerElement({ + icon: faChargingStation, + color: '#008800', + }), + }) + .setLngLat([ + evChargingStation.longitude, + evChargingStation.latitude, + ]) + .addTo(mapRef.current!); + } + }); + } + } catch (error) { + console.error('Error loading EV charging stations:', error); + } + }; + + loadEVChargingStations(); + }, []); + + useEffect(() => { + const loadRatSightings = async () => { + try { + const ratSightings = await getRatSightings(); + + if (ratSightings) { + ratSightings.forEach((ratSighting) => { + if ( + ratSighting.location?.coordinates[0] && + ratSighting.location?.coordinates[1] + ) { + new mapboxgl.Marker({ + element: MarkerElement({ icon: faBugs, color: '#000000' }), + }) + .setLngLat(ratSighting.location.coordinates) + .addTo(mapRef.current!); + } + }); + } + } catch (error) { + console.error('Error loading rat sightings:', error); + } + }; + + loadRatSightings(); + }, []); + return
; } diff --git a/components/Marker.tsx b/components/Marker.tsx new file mode 100644 index 0000000..2259bb7 --- /dev/null +++ b/components/Marker.tsx @@ -0,0 +1,61 @@ +import type { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { renderToString } from 'react-dom/server'; + +export function MarkerElement({ + color, + icon, +}: { + color?: string; + icon?: IconProp; +}) { + const marker = renderToString(); + const markerElement = document.createElement('div'); + markerElement.innerHTML = marker; + + return markerElement; +} + +export function Marker({ + color = '#ff0000', + icon, +}: { + color?: string; + icon?: IconProp; +}) { + return ( +
+ + + + + + + + + + + {!icon && } + + {icon && ( + + )} +
+ ); +} diff --git a/package-lock.json b/package-lock.json index 7a8d772..f3f0ffa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,11 @@ "name": "glenzz", "version": "0.1.0", "dependencies": { + "@fortawesome/fontawesome-svg-core": "^6.7.2", + "@fortawesome/free-brands-svg-icons": "^6.7.2", + "@fortawesome/free-regular-svg-icons": "^6.7.2", + "@fortawesome/free-solid-svg-icons": "^6.7.2", + "@fortawesome/react-fontawesome": "^0.2.2", "lucide-react": "^0.473.0", "mapbox": "^1.0.0-beta10", "mapbox-gl": "^3.9.3", @@ -311,6 +316,76 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz", + "integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz", + "integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==", + "license": "MIT", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.7.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-brands-svg-icons": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.7.2.tgz", + "integrity": "sha512-zu0evbcRTgjKfrr77/2XX+bU+kuGfjm0LbajJHVIgBWNIDzrhpRxiCPNT8DW5AdmSsq7Mcf9D1bH0aSeSUSM+Q==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.7.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-regular-svg-icons": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.7.2.tgz", + "integrity": "sha512-7Z/ur0gvCMW8G93dXIQOkQqHo2M5HLhYrRVC0//fakJXxcF1VmMPsxnG6Ee8qEylA8b8Q3peQXWMNZ62lYF28g==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.7.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz", + "integrity": "sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.7.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/react-fontawesome": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.2.tgz", + "integrity": "sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "@fortawesome/fontawesome-svg-core": "~1 || ~6", + "react": ">=16.3" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -4026,7 +4101,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -4202,7 +4276,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -4537,7 +4610,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5124,7 +5196,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -5200,7 +5271,6 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, "license": "MIT" }, "node_modules/read-cache": { diff --git a/package.json b/package.json index fd226b3..bdc59db 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,11 @@ "format": "prettier --write ." }, "dependencies": { + "@fortawesome/fontawesome-svg-core": "^6.7.2", + "@fortawesome/free-brands-svg-icons": "^6.7.2", + "@fortawesome/free-regular-svg-icons": "^6.7.2", + "@fortawesome/free-solid-svg-icons": "^6.7.2", + "@fortawesome/react-fontawesome": "^0.2.2", "lucide-react": "^0.473.0", "mapbox": "^1.0.0-beta10", "mapbox-gl": "^3.9.3", From 93c4b3e9eeffee7f61d9f5854eff07ec249c0be1 Mon Sep 17 00:00:00 2001 From: KaBankz <20971850+KaBankz@users.noreply.github.com> Date: Tue, 21 Jan 2025 20:22:33 -0500 Subject: [PATCH 11/14] Add sidebar component --- app/maps/page.tsx | 64 +++++++++++++++++++++++++++++++----------- components/Marker.tsx | 2 +- components/SideBar.tsx | 53 ++++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 17 deletions(-) create mode 100644 components/SideBar.tsx diff --git a/app/maps/page.tsx b/app/maps/page.tsx index e8b286f..334a3e1 100644 --- a/app/maps/page.tsx +++ b/app/maps/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { faBolt, @@ -17,13 +17,19 @@ import { getRatSightings, } from '@/app/maps/actions'; import { MarkerElement } from '@/components/Marker'; +import { SideBar } from '@/components/SideBar'; import 'mapbox-gl/dist/mapbox-gl.css'; export default function MapsPage() { + const [isSidebarOpen, setIsSidebarOpen] = useState(false); const mapContainerRef = useRef(null); const mapRef = useRef(null); + const toggleSidebar = () => { + setIsSidebarOpen(!isSidebarOpen); + }; + useEffect(() => { mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN; @@ -36,6 +42,14 @@ export default function MapsPage() { }); mapRef.current.addControl(new mapboxgl.NavigationControl()); + + mapRef.current.on('click', (e) => { + const target = e.originalEvent.target as HTMLElement; + if (target.closest('.mapboxgl-marker')) { + return; + } + setIsSidebarOpen(false); + }); } return () => { @@ -54,11 +68,15 @@ export default function MapsPage() { project.georeference?.coordinates[0] && project.georeference?.coordinates[1] ) { - new mapboxgl.Marker({ + const marker = new mapboxgl.Marker({ element: MarkerElement({ icon: faBolt, color: '#ccac00' }), }) .setLngLat(project.georeference.coordinates) .addTo(mapRef.current!); + + marker.getElement().addEventListener('click', () => { + setIsSidebarOpen(true); + }); } }); } @@ -78,7 +96,7 @@ export default function MapsPage() { if (outages) { outages.forEach((outage) => { if (outage.latitude && outage.longitude) { - new mapboxgl.Marker({ + const marker = new mapboxgl.Marker({ element: MarkerElement({ icon: faPlugCircleXmark, color: '#ff0000', @@ -86,6 +104,10 @@ export default function MapsPage() { }) .setLngLat([outage.longitude, outage.latitude]) .addTo(mapRef.current!); + + marker.getElement().addEventListener('click', () => { + setIsSidebarOpen(true); + }); } }); } @@ -103,19 +125,20 @@ export default function MapsPage() { const evChargingStations = await getEVChargingStations(); if (evChargingStations) { - evChargingStations.forEach((evChargingStation) => { - if (evChargingStation.latitude && evChargingStation.longitude) { - new mapboxgl.Marker({ + evChargingStations.forEach((station) => { + if (station.latitude && station.longitude) { + const marker = new mapboxgl.Marker({ element: MarkerElement({ icon: faChargingStation, color: '#008800', }), }) - .setLngLat([ - evChargingStation.longitude, - evChargingStation.latitude, - ]) + .setLngLat([station.longitude, station.latitude]) .addTo(mapRef.current!); + + marker.getElement().addEventListener('click', () => { + setIsSidebarOpen(true); + }); } }); } @@ -133,16 +156,20 @@ export default function MapsPage() { const ratSightings = await getRatSightings(); if (ratSightings) { - ratSightings.forEach((ratSighting) => { + ratSightings.forEach((sighting) => { if ( - ratSighting.location?.coordinates[0] && - ratSighting.location?.coordinates[1] + sighting.location?.coordinates[0] && + sighting.location?.coordinates[1] ) { - new mapboxgl.Marker({ + const marker = new mapboxgl.Marker({ element: MarkerElement({ icon: faBugs, color: '#000000' }), }) - .setLngLat(ratSighting.location.coordinates) + .setLngLat(sighting.location.coordinates) .addTo(mapRef.current!); + + marker.getElement().addEventListener('click', () => { + setIsSidebarOpen(true); + }); } }); } @@ -154,5 +181,10 @@ export default function MapsPage() { loadRatSightings(); }, []); - return
; + return ( + <> +
+ + + ); } diff --git a/components/Marker.tsx b/components/Marker.tsx index 2259bb7..829e2ac 100644 --- a/components/Marker.tsx +++ b/components/Marker.tsx @@ -24,7 +24,7 @@ export function Marker({ icon?: IconProp; }) { return ( -
+
diff --git a/components/SideBar.tsx b/components/SideBar.tsx new file mode 100644 index 0000000..762f88a --- /dev/null +++ b/components/SideBar.tsx @@ -0,0 +1,53 @@ +import Image from 'next/image'; + +import { faXmark } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { AnimatePresence, motion } from 'motion/react'; + +export function SideBar({ + isOpen, + onClose, +}: { + isOpen: boolean; + onClose: () => void; +}) { + return ( + + {isOpen && ( + +
+
+ placeholder + +
+

Sidebar

+
+ + )} + + ); +} From b1a0727eb99fff3888ae542b0b3412063f0f6185 Mon Sep 17 00:00:00 2001 From: KaBankz <20971850+KaBankz@users.noreply.github.com> Date: Tue, 21 Jan 2025 20:43:58 -0500 Subject: [PATCH 12/14] Added focus on selected marker --- app/maps/page.tsx | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/app/maps/page.tsx b/app/maps/page.tsx index 334a3e1..26474aa 100644 --- a/app/maps/page.tsx +++ b/app/maps/page.tsx @@ -30,6 +30,14 @@ export default function MapsPage() { setIsSidebarOpen(!isSidebarOpen); }; + const focusOnMarker = (coordinates: [number, number]) => { + mapRef.current?.flyTo({ + center: coordinates, + zoom: 14, + duration: 1500, + }); + }; + useEffect(() => { mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN; @@ -68,14 +76,16 @@ export default function MapsPage() { project.georeference?.coordinates[0] && project.georeference?.coordinates[1] ) { + const coordinates = project.georeference.coordinates; const marker = new mapboxgl.Marker({ element: MarkerElement({ icon: faBolt, color: '#ccac00' }), }) - .setLngLat(project.georeference.coordinates) + .setLngLat(coordinates) .addTo(mapRef.current!); marker.getElement().addEventListener('click', () => { setIsSidebarOpen(true); + focusOnMarker(coordinates); }); } }); @@ -96,17 +106,22 @@ export default function MapsPage() { if (outages) { outages.forEach((outage) => { if (outage.latitude && outage.longitude) { + const coordinates: [number, number] = [ + outage.longitude, + outage.latitude, + ]; const marker = new mapboxgl.Marker({ element: MarkerElement({ icon: faPlugCircleXmark, color: '#ff0000', }), }) - .setLngLat([outage.longitude, outage.latitude]) + .setLngLat(coordinates) .addTo(mapRef.current!); marker.getElement().addEventListener('click', () => { setIsSidebarOpen(true); + focusOnMarker(coordinates); }); } }); @@ -127,17 +142,22 @@ export default function MapsPage() { if (evChargingStations) { evChargingStations.forEach((station) => { if (station.latitude && station.longitude) { + const coordinates: [number, number] = [ + station.longitude, + station.latitude, + ]; const marker = new mapboxgl.Marker({ element: MarkerElement({ icon: faChargingStation, color: '#008800', }), }) - .setLngLat([station.longitude, station.latitude]) + .setLngLat(coordinates) .addTo(mapRef.current!); marker.getElement().addEventListener('click', () => { setIsSidebarOpen(true); + focusOnMarker(coordinates); }); } }); @@ -161,14 +181,16 @@ export default function MapsPage() { sighting.location?.coordinates[0] && sighting.location?.coordinates[1] ) { + const coordinates = sighting.location.coordinates; const marker = new mapboxgl.Marker({ element: MarkerElement({ icon: faBugs, color: '#000000' }), }) - .setLngLat(sighting.location.coordinates) + .setLngLat(coordinates) .addTo(mapRef.current!); marker.getElement().addEventListener('click', () => { setIsSidebarOpen(true); + focusOnMarker(coordinates); }); } }); From 6110511e8a52a8f8f22f8e55bec509839f7a1258 Mon Sep 17 00:00:00 2001 From: KaBankz <20971850+KaBankz@users.noreply.github.com> Date: Tue, 21 Jan 2025 20:50:54 -0500 Subject: [PATCH 13/14] Remove unused function --- types/outages.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/types/outages.ts b/types/outages.ts index 4b8ae43..739b805 100644 --- a/types/outages.ts +++ b/types/outages.ts @@ -1,17 +1,5 @@ import { z } from 'zod'; -/* Helper function to coerce an object to a zod schema **/ -function coerceObject( - shape: T, - params?: z.RawCreateParams -) { - return new z.ZodEffects({ - schema: z.object(shape, params), - effect: { type: 'preprocess', transform: Object }, - typeName: z.ZodFirstPartyTypeKind.ZodEffects, - }); -} - export const OutageSchema = z .object({ /** From 2beabb70e4123421513ff62db61762ac20f41f8e Mon Sep 17 00:00:00 2001 From: KaBankz <20971850+KaBankz@users.noreply.github.com> Date: Tue, 21 Jan 2025 20:55:38 -0500 Subject: [PATCH 14/14] Move new maps to v2 route --- app/{maps => map/v2}/actions.ts | 0 app/{maps => map/v2}/page.tsx | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename app/{maps => map/v2}/actions.ts (100%) rename app/{maps => map/v2}/page.tsx (99%) diff --git a/app/maps/actions.ts b/app/map/v2/actions.ts similarity index 100% rename from app/maps/actions.ts rename to app/map/v2/actions.ts diff --git a/app/maps/page.tsx b/app/map/v2/page.tsx similarity index 99% rename from app/maps/page.tsx rename to app/map/v2/page.tsx index 26474aa..fe02e90 100644 --- a/app/maps/page.tsx +++ b/app/map/v2/page.tsx @@ -15,7 +15,7 @@ import { getOutages, getProjects, getRatSightings, -} from '@/app/maps/actions'; +} from '@/app/map/v2/actions'; import { MarkerElement } from '@/components/Marker'; import { SideBar } from '@/components/SideBar';