Skip to main content

Fishing Regulations & Master Angler System Documentation

Overview

This system provides comprehensive support for state fishing regulations, zone management, and Master Angler programs. It enables location-based regulation lookup, annual regulation updates, and automatic Master Angler qualification checking.

Research Findings

Geospatial Data Availability: ✅ YES, coordinates exist!

  • NOAA Fisheries provides GIS datasets for fishing zones
  • State DNR agencies provide geospatial boundary data
  • Fishing zones can be defined using:
    • Simple: Center point + radius (circular zones)
    • Moderate: Bounding box (rectangular zones)
    • Complex: Polygon coordinates (irregular boundaries)

Master Angler Programs: Programs exist in multiple states:

  • Michigan: Master Angler Program (certificate + patch)
  • Wisconsin: Trophy Fish Program
  • Minnesota: Master Angler Program
  • Other states: Various catch recognition programs

Core Entities

State

Represents a state/province with fishing regulations.

Key Features:

  • State information (name, abbreviation, country)
  • DNR/Wildlife agency contact information
  • Master Angler program information
  • Regulation year tracking
  • Links to zones and regulations

FishingZone

Represents a fishing zone/area within a state.

Boundary Types (supports all three):

  1. Circular: CenterLatitude, CenterLongitude, RadiusMeters
  2. Rectangular: NorthBound, SouthBound, EastBound, WestBound
  3. Polygon: Collection of ZoneBoundary points

Features:

  • Zone hierarchy (parent/child zones)
  • Water body information
  • Regulation year tracking
  • Multiple boundary methods for flexibility

ZoneBoundary

Stores polygon coordinate points for complex zone boundaries.

Usage: When zones have irregular shapes, store multiple coordinate points in order to form a polygon.

FishingRegulation

Represents species-specific regulations for a zone.

Key Features:

  • Bag limits (daily, possession)
  • Size limits (minimum, maximum, slot limits)
  • Season dates (via RegulationSeason)
  • Gear restrictions
  • Special rules

RegulationSeason

Date ranges when regulations apply.

Features:

  • Start/end dates
  • Time restrictions (optional)
  • Day of week restrictions
  • Multiple seasons per regulation

MasterAnglerProgram

State Master Angler or catch recognition program.

Features:

  • Program information and requirements
  • Recognition types (certificate, patch, pin, letter, trophy)
  • Program year tracking
  • Links to species requirements

MasterAnglerSpecies

Size requirements for species in Master Angler program.

Features:

  • Minimum length/weight requirements
  • Length OR weight qualification options
  • Program year tracking

MasterAnglerSubmission

User's submission to Master Angler program.

Features:

  • Links to catch log entry
  • Submission status tracking
  • Official program response tracking
  • Recognition received tracking

Usage Examples

Finding Regulations for User's Location

public async Task<FishingRegulation?> GetRegulationForLocation(
double latitude,
double longitude,
int stateId,
int? speciesId,
DateOnly date)
{
// Find zone user is in
var zone = await FindZoneForLocation(latitude, longitude, stateId);

if (zone == null)
return null;

// Find regulation for zone and species
var regulation = await context.FishingRegulations
.Include(r => r.Seasons)
.FirstOrDefaultAsync(r =>
r.StateId == stateId
&& r.ZoneId == zone.Id
&& (r.FishSpeciesId == speciesId || r.FishSpeciesId == null)
&& r.RegulationYear == DateTime.Now.Year
&& r.IsActive);

// Check if date is within any active season
if (regulation != null)
{
var activeSeason = regulation.Seasons
.FirstOrDefault(s =>
s.IsActive
&& date >= s.StartDate
&& date <= s.EndDate);

if (activeSeason == null)
return null; // Season closed
}

return regulation;
}

private async Task<FishingZone?> FindZoneForLocation(
double latitude,
double longitude,
int stateId)
{
// Try circular zones first
var circularZone = await context.FishingZones
.Where(z => z.StateId == stateId
&& z.CenterLatitude.HasValue
&& z.CenterLongitude.HasValue
&& z.RadiusMeters.HasValue)
.ToListAsync();

foreach (var zone in circularZone)
{
var distance = CalculateDistance(
latitude, longitude,
zone.CenterLatitude.Value,
zone.CenterLongitude.Value);

if (distance <= zone.RadiusMeters.Value)
return zone;
}

// Try bounding box
var boundingBoxZone = await context.FishingZones
.FirstOrDefaultAsync(z =>
z.StateId == stateId
&& z.NorthBound.HasValue
&& z.SouthBound.HasValue
&& z.EastBound.HasValue
&& z.WestBound.HasValue
&& latitude >= z.SouthBound.Value
&& latitude <= z.NorthBound.Value
&& longitude >= z.WestBound.Value
&& longitude <= z.EastBound.Value);

if (boundingBoxZone != null)
return boundingBoxZone;

// Try polygon zones (point-in-polygon algorithm)
var polygonZones = await context.FishingZones
.Include(z => z.Boundaries.OrderBy(b => b.PointOrder))
.Where(z => z.StateId == stateId && z.Boundaries.Any())
.ToListAsync();

foreach (var zone in polygonZones)
{
if (IsPointInPolygon(latitude, longitude, zone.Boundaries))
return zone;
}

return null;
}

private bool IsPointInPolygon(double lat, double lon, ICollection<ZoneBoundary> boundaries)
{
// Ray casting algorithm for point-in-polygon
var points = boundaries.OrderBy(b => b.PointOrder).ToList();
bool inside = false;

for (int i = 0, j = points.Count - 1; i < points.Count; j = i++)
{
if (((points[i].Latitude > lat) != (points[j].Latitude > lat)) &&
(lon < (points[j].Longitude - points[i].Longitude) *
(lat - points[i].Latitude) / (points[j].Latitude - points[i].Latitude) +
points[i].Longitude))
{
inside = !inside;
}
}

return inside;
}

Checking Master Angler Qualification

public async Task<bool> CheckMasterAnglerQualification(
Guid userId,
Guid catchLogEntryId,
int stateId)
{
var catchDetail = await context.CatchDetails
.Include(cd => cd.FishingLogEntry)
.FirstOrDefaultAsync(cd => cd.FishingLogEntryId == catchLogEntryId);

if (catchDetail == null)
return false;

var program = await context.MasterAnglerPrograms
.Include(p => p.Species)
.FirstOrDefaultAsync(p =>
p.StateId == stateId
&& p.ProgramYear == DateTime.Now.Year
&& p.IsActive
&& p.IsAcceptingSubmissions);

if (program == null)
return false;

var speciesRequirement = program.Species
.FirstOrDefault(s =>
s.FishSpeciesId == catchDetail.FishSpeciesId
&& s.IsActive);

if (speciesRequirement == null)
return false;

// Check length requirement
bool qualifiesByLength = catchDetail.LengthInches.HasValue &&
catchDetail.LengthInches.Value >= speciesRequirement.MinimumLengthInches;

// Check weight requirement (if applicable)
bool qualifiesByWeight = catchDetail.WeightLbs.HasValue &&
speciesRequirement.MinimumWeightLbs.HasValue &&
catchDetail.WeightLbs.Value >= speciesRequirement.MinimumWeightLbs.Value;

// Check if length OR weight qualifies
if (speciesRequirement.LengthOrWeight)
return qualifiesByLength || qualifiesByWeight;

// Both must qualify
return qualifiesByLength &&
(!speciesRequirement.MinimumWeightLbs.HasValue || qualifiesByWeight);
}

Annual Regulation Update Process

public async Task UpdateRegulationsForYear(int stateId, int newYear)
{
// Archive old regulations
var oldRegulations = await context.FishingRegulations
.Where(r => r.StateId == stateId && r.RegulationYear < newYear)
.ToListAsync();

foreach (var reg in oldRegulations)
{
reg.IsActive = false;
reg.ExpirationDate = DateTime.UtcNow;
}

// Copy structure for new year (can be modified)
var newRegulations = oldRegulations.Select(r => new FishingRegulation
{
StateId = r.StateId,
ZoneId = r.ZoneId,
FishSpeciesId = r.FishSpeciesId,
RegulationYear = newYear,
DailyBagLimit = r.DailyBagLimit,
MinimumLengthInches = r.MinimumLengthInches,
// ... copy other fields
IsActive = true,
EffectiveDate = new DateTime(newYear, 1, 1)
}).ToList();

context.FishingRegulations.AddRange(newRegulations);
await context.SaveChangesAsync();

// Update state's current regulation year
var state = await context.States.FindAsync(stateId);
if (state != null)
{
state.CurrentRegulationYear = newYear;
state.LastRegulationUpdate = DateTime.UtcNow;
await context.SaveChangesAsync();
}
}

Annual Update Strategy

  1. Regulation Year Tracking: Each regulation has a RegulationYear field
  2. Bulk Update: Copy previous year's regulations, modify as needed
  3. Archive Old Data: Set IsActive = false for old regulations
  4. Zone Boundaries: Usually don't change, but can be updated if needed
  5. Master Angler Programs: Update ProgramYear and species requirements

Data Sources

  • NOAA Fisheries: GIS data for federal waters
  • State DNR Websites: Official regulation PDFs and web pages
  • State GIS Portals: Geospatial boundary data
  • Master Angler Program Websites: Official program requirements

Benefits

  1. Location-Based Lookup: Automatically show regulations based on GPS location
  2. Annual Updates: Easy to update regulations each year
  3. Master Angler Integration: Automatic qualification checking
  4. Compliance: Help users stay compliant with regulations
  5. Geospatial Flexibility: Supports simple to complex zone boundaries